How to Debug Effectively?
Is The Error in The Code or in The Coder?
As developers one of the main consequences of unhappiness at work is being stuck on a problem, now imagine this with a deadline. Usually we tend to want to fix things quickly even if we don’t know exactly how it’s working.
Lack of understanding of the problem, how to make it happen and finding the most decent solution, not the easiest or hardest, are the main challenges we face when finding and solving bugs.
Effective debugging consists of following a process in which we make sure at all times that we are on the right path to fix the bug, starting from code with no warnings, calmly making the bug reproducible, reading the error message carefully and understanding it, using breakpoints, testing to code and if there is no way we can solve it on our own by asking the right person for help.
What is debugging?
In computer programming and software development, debugging is the process of finding and resolving bugs (defects or problems that prevent correct operation) within computer programs, software, or systems.
The terms “bug” and “debugging” are popularly attributed to Admiral Grace Hopper in the 1940s. While she was working on a Mark II computer at Harvard University, her associates discovered a moth stuck in a relay and thereby impeding operation, whereupon she remarked that they were “debugging” the system.
Nowadays we still have bugs in the system, albeit not the flying kind.
Remember that no one write perfect software, so it’s a given that debugging will take up a major portion of your day. Let’s take a look at some of the issues involved in debugging and some general strategies for finding elusive bugs and fixing it.
Type of errors and how to avoid them?
Logical Errors
A logical error is an error in a program’s source code that gives way to an unanticipated and erroneous behaviour. A logical error is classified as a type of runtime error that can result in a program producing incorrect output. It can also cause the program to crash when running.
Example:
What is the error?
As we can see the function expected to return the sum of the numbers in the array but the for loop isn’t counting the first element, for that reason the print statement will be false.
How we can avoid this error?
Initialising the for loop in i=0
Syntax Errors
A syntax error occurs when during compilation the compiler finds something in your code it doesn’t know what it means, or it not sure how to translate what you wrote. The compiler will always advise you about syntax errors, even though sometimes it could be wrong about the line (or lines) where the error occurred.
Example:
What is the error?
In the line 11:40 we are trying to compare if the result of sumNumbersInArray is equal to 10, but in JavaScript doesn’t exist the operator ====.
How we can avoid this error?
Removing an equal operator (===).
Compilation Errors
Compilation is the process of converting a high-level coding language into a lower-level language that can be better understood by the computer. Compilation errors occur when the compiler isn’t able to properly transform the high-level code into the lower-level one. This prevents the software from being launched or tested.
How to avoid them?
Understand the language well. One of the most important ways to avoid compilation failures in any language is to understand the language well.
Runtime Errors
These bugs occur when the code “won’t play nice” with another computer, even if it worked perfectly fine on the developer’s own computer. These errors are especially frustrating because they directly impact the end user and make the application appear unreliable or even completely broken.
How to avoid them?
- Avoid using variables that have not been initialised. These may be set to 0 on your system but not on the coding platform.
- Check every single occurrence of an array element and ensure that it is not out of bounds.
- Avoid declaring too much memory. Check for the memory limit specified in the question.
- Avoid declaring too much Stack Memory. Large arrays should be declared globally outside the function.
- Use return as the end statement.
- Avoid referencing free memory or null pointers.
Arithmetic Errors
These errors are just like logic errors, but with mathematics. For example, a division equation may require the computer to divide by zero. Since this is mathematically impossible, it results in an error that prevents the software from working correctly.
How to avoid them?
The best way to avoid computational errors is to avoid computation. Develop general algorithms for whatever quantity that you are looking for and then proceed to “plug and chug” as the last step. Mathematics requires precision, however, and you often cannot avoid having to comb over your work tediously.
Resource Errors
Sometimes, a program can force the computer it’s running on to attempt to allocate more resources (processor power, random access memory, disk space, etc.) than it has. This results in the program becoming bugged or even causes the entire system to crash.
How to avoid them?
Optimisation is the key to prevent this kind of error.
Interface Errors
These bugs typically happen when the inputs the software receives do not conform to the accepted standards. When handled incorrectly, these errors can look like errors on your side even when they’re on the caller’s side, and vice versa.
Example:
What is the error?
In the line 3 we define the age in the user interface as string type, the in the line 11 we are passing to the function printUser an age in number type.
How we can avoid this type of errors?
Following the contract with the interface in this case passing the age as (string) “22”.
Debugging tools
Print Statements
Print statement debugging is a process in which a developer instruments their application with “printf” statements to generate output while the program is executing.
Using print statements is a good way to trace through your code and troubleshoot potential problems. In addition, print statements can be used for logging and diagnostic purposes.
Developers will often use print debugging to get insight into what’s happening in the processes that they’re running. They will print out when certain functions are called, the values of function arguments, values of variables used in algorithms and when messages are exchanged with other processes. But, at scale, and in complex systems with concurrent processes, Print debugging quickly becomes unwieldy to use as the sole debugging method.
Pros:
- Easy to add
- Easy to see what is happening in the program as it runs.
- Levels of print output can be controlled.
Cons:
- Creates an additional maintenance point in the code.
- Difficult to decipher what is happening when too much output is generated.
- Difficult to Determine Process Sequences.
Breakpoints
Breakpoints are one of the most important debugging techniques in your developer’s toolbox. You set breakpoints wherever you want to pause debugger execution. For example, you may want to see the state of code variables or look at the call stack at a certain breakpoint.
Using a debugger is the best way to find the hardest bugs, but it is also the most advanced method and is not needed when other techniques will work.
There are a few more tricks to know when it comes to debugging: conditional breakpoints, hit counters, and more! These advanced breakpoint types help reduce the time required to debug a certain application state or logic flow, letting us be more productive while debugging.
Conditional breakpoints: Suppose that in our application’s codebase, we’re only interested in pausing execution at a breakpoint when a certain condition is true. For example, we may only want to pause execution at a breakpoint in a loop when we’re working with a specific value to debug one specific case based on that value.
Hit counters: When working with loops or when iterating over a collection of items coming from the database, it can be useful to only pause program execution the first couple of times, or only after we’ve iterated over more than 1.000 items.
Pros:
- Debugging is the Swiss Army knife of a developer. Breakpoints are excellent tools to query internal state or to call APIs at a given point of the code. With dynamic code evaluation, a method or function can be easily called with the relevant input data to investigate its results.
- You can inspect the entire program state, not just the values you thought to print out in advance. This can massively speed up the debugging process by reducing the feedback cycle to less than a second. Especially important when it takes some time to reach the bug.
- You can see where calls came from and even inspect values up the stack trace.
Cons:
- It’s most to use breakpoints in local debugging sessions only.
- It require manual intervention to interrogate the system under debugging to get the relevant data each time the program stops at a breakpoint. This two makes really hard to catch sporadic events happening over a long time.
- If you debug something today, then tomorrow you can’t depend on this knowledge as the system might behave completely differently.
Robert C. Martin shares similar views about debuggers. The following quote is from his post Debuggers are a wasteful Timesink from 2003:
I consider debuggers to be a drug — an addiction. Programmers can get into the horrible habit of depending on the debugger instead of on their brain. IMHO a debugger is a tool of last resort. Once you have exhausted every other avenue of diagnosis, and have given very careful thought to just rewriting the offending code, then you may need a debugger.
Debugging principles
Fix The Problem, Don’t Blame
It’s not about your colleagues, it’s not about you, it’s a problem and you have to solve it. Taking responsibility when things go wrong is crucially important to building trust with others and learning from your mistakes.
The next time you contact the people directly above you, instead of asking if those lazy people at the top have already set things right, you should ask: “What can we do to fix this today?
Don’t Panic
As a beginning programmer, or operations (or devops) person it can be overwhelming to deal with logs, messages, metrics and other possible relevant information that is coming at you at such a point.
Pause, breath, ponder, choose, do.
Even when you are in panic mode, try to keep a scientific approach to debugging. Check your assumptions, generate hypothesis of what causes the problem and check that hypothesis. But this is not a scientific paper, there must be rigor, but also speed; employ the ‘theory of close enough’, formulate hypotheses quickly and verify them.
Observe (1) what happened, (2) what happens and (3) what should happen. Orient yourself to the problem by applying your knowledge of the system to the observations, Decide on what the cause is and Act on it. Then Observe again to see if you were right. This is a generic approach known as the OODA loop (Observe, Orient, Decide, Act) and it was originally described by an American Air Force Colonel. But it did not stay in the military, this approach is used in litigation, law enforcement, and business. I think it works as a generic strategy for debugging too. The main point is you should not jump to conclusions too quickly.
Do not Waste Time on a Silly Bug
The World Keeps on Turning
This will not be your first bug and neither the last one, don’t take things personals.
Take breaks, sitting +8 hours doesn’t make you more productive, frequent breaks give the mind and body time to recharge.
Taking breaks at work does increase productivity, even if machines and computers are idle for a few minutes. The short time away gives employees the chance to stretch tired muscles, find relief from sustained positions and postures and retain any information they might have learned in the last hour or so.
Working more hours is also bad idea, the most common situation is get born-out, and there are more side effects as: can make you more likely to drink, decreasing your productivity, it’s very hard to quiet your mind at bedtime, your stress will increase, etc.
Take responsibility, but don’t be silly.
Debugging strategies
Start Point
We must start in a clean environment, with no changes, no errors, and no warnings unrelated to the problem we are trying to solve.
When you trying to solve a problem, you need to gather all the relevant data.
Once you think you know what is going on, it’s time to find out what the software thinks is going on.
Make it Reproducible
Relax, this doesn’t mean our bugs are really multiplying. We’re talking about a different kind of reproduction.
The best way to start fixing a bug is to make it reproducible. After all, if you can’t reproduce it, how will you know if it is ever fixed?
Read and Understand The Error Message
READ THE F*CK ERROR MESSAGE
What does it say, what does it mean, on which line is it happening? That line really exists (are you kidding?).
What if it’s not a crash? What if it’s just a bad result?
Get in there with a debugger and use your failing test to trigger the problem.
Before anything else, make sure that you’re also seeing the incorrect value in the debugger.
Talk to the rubber duck
A very simple but particularly useful technique for finding the cause of a problem is simply to explain it to someone else or just imagine that you are explaining the problem to someone else.
The simple act of explaining, step by step, what the code is supposed to do often cause the problem to leap off the screen and announce itself.
Divide & Conquer
Divide and conquer is an algorithm design type. It works by recursively breaking a problem in two (or more) smaller problems of a similar nature.
The straightforward use of divide and conquer in debugging is to split the codebase in half, and add a log there. If the log works and prints correct data, we can assume that everything up to that point works fine; if not, the error is in the first half of the code.
If your team introduced a bug during a set of releases, you can use the same type of technique. Create a test that causes the current release to fail. Then choose a half-way release between now and the last known working version. Run the test again, and decide how narrow your search.
Test to Code
The simple concept of TDD (Test Driven Development) is write tests to validate codes. This helps to avoid duplication of code as we write a small amount of code at a time in order to pass tests.
There are three general steps in TDD:
- [RED], this means we implement the unit tests first. It’s called [RED] because we haven’t had the code implementations yet. So, the unit tests will fail at first. We need to note that we must find various conditions in creating the unit tests to have better code quality. Think and create the unit tests just like you are a naughty user trying to find bugs in your system for your own sake, with all possibilities.
- [GREEN], implement the minimum code to pass the tests. If the code is still failing to pass the tests, write more code (but still the minimum code) to pass the tests. By following this, we can make sure our implementation doesn’t have duplicate codes.
- [REFACTOR], do this if there are still possibilities to enhance and improve your code without failing the tests. Make sure our new, enhanced, and improved implementation doesn’t change the original behavior of our old implementation.
Google it
Google/duckduckgo or whatever search engine you use is your best friend.
How to make a good search?
- Use the Keyword related: to find similar websites.
- Use the Keyword search: to search within a website.
- Use the Keyword … to search within a time frame.
- Use the keyword * to replace missing words.
- If you’re looking for a definition of a word, use the keyword define: to find a definition.
- Use site: to search for a particular website or content.
Places like StackOverflow and the oficial documentation of the technology that you are using is most of the time helpful.
DON’T Get Stuck, Ask!
I don’t think there is a specific minimum or maximum time to ask for help, but make sure you do your homework before asking for help. It doesn’t look good to ask your colleagues when you haven’t even tried.
When you can’t fix something, you should ask for help, don’t waste your time trying to solve something that is beyond your expertise.
Your colleagues are also your best friends (at least at work).
How to ask for help at work?
- Do your homework.
The first step to asking for help isn’t to ask — it’s to confirm if your question is worth asking. This means doing your homework.
- Find the best person at the best time.
Once you have a question that makes sense to ask, the next step is to identify the least disruptive — and therefore most effective — way to approach other people. Here, it can be helpful to ask yourself three questions:
- Who is the best person to ask?
- When is the best time to ask?
- Where is the best place to ask?
- Show your homework (and your gratitude).
When it comes to asking your question, style can be as important as substance. How you frame your question can mean the difference between getting the help you need (and not) and building a good professional reputation (and not). To make the best impression possible, don’t just ask your question; share all the hard work you’ve done to help yourself before involving other people.
Embrace the fact that debugging is just a problem solving, and attack it as such.
Thanks for reading, and happy debugging!
👋 Let’s be friends! Follow me on Twitter and connect with me on LinkedIn. Don’t forget to follow me here on Medium as well.
References
- https://en.wikipedia.org/wiki/Debugging
- https://www.r-bloggers.com/2022/04/dont-panic-a-scientific-approach-to-debugging-production-failure/
- https://www.work-fit.com/blog/how-effective-breaks-at-work-increase-productivity
- https://www.healthline.com/health/working-too-much-health-effects#5.-Your-hearts-working-overtime,-too-
- https://hbr.org/2021/04/how-to-ask-for-help-at-work