In this article I’ve gathered my thoughts to help developers to get better in debugging especially if they find debugging really hard. The article also lists a set of tools which will help them to debug efficiently and to improve their skills. Hopefully you will find some value in this article, if you are also looking for debugging advice.
How, where, and why?
Don’t just start debugging. Understand the bug. Make sure you get all the details from the bug reporter(e.g. a QA engineer who reported the bug).
Reproduce the bug. If it turns out to be a flaky bug, try to find a pattern. Flaky bugs tend to be caused by timing and asynchronous processing issues. So play with the variables related to them.
After you have a clear understanding of the problem and you can reproduce the bug confidently, we need to figure out the sections where the bug is happening. If you a bunch is areas that you suspect the bug resides, arrange the sections in a logical order and try divide and conquer approach to eliminate the sections – So you reduce the time testing all the sections.
Some annoying forms of bugs are when the bug only happens on a type of a particular platform (e.g. Production vs Development, Firefox vs Chrome). In these bugs, you have to remember that the bug is particularly relates to the specific platform. So you have to consider the things that are different in that platform. Ideally you would need to debug on that platform.
After a debug session, look back and think about the approach you took to debug and find the bug. Think about what you could have done better and make notes.
About the Fix
Once you understand or learned how the bug occurs, you might be tempted to quickly fix the bug and move on.
However you see the bug is there in the first place because someone didn’t think that the bug will occur. So your fix should ideally help to reduce from things like that happening. Often this means to address the root cause of the bug. If your fix doesn’t address the root cause, your fix might actually just be a monkey patch. It will be a matter of time where a similar bug occurs due to the fact that root cause is still not fixed.
Essentials for your debugging toolbox
Next I have gathered some tools which will help you in your debugging.
Use a debugger
A debugger is there to debug your code and make your life easier. It allows you to pause the code execution. You can verify the state of the application and jump to different code execution points during the debugging session.
If you are using an IDE, it would be easier to use the inbuilt one with the option to put breakpoints in editor.
Otherwise in a terminal code editor, you would need to put debug statements (e.g. binging.pry
for Pry debugger)
Some IDEs also come with a specific mode called ‘Debug Mode’ and typically you can use the debugger in this mode. Another interesting aspect is that, you might have the option to automatically pause when an exception occurs. This is another handy feature when you like to jump to statement when exactly it fails.
Git bisect
git bisect
is a powerful way to jump from one state of the code repository to another state by dividing the commit range by half every time you move to the next step. This will come handy when you know a commit in history where the bug is not reproducible (system is working as expected).
So basically with this command you can specify that location as the good state and current location as the bad state. Git with automatically divide commit range and checkout the middle commit depending on whether the current state is bad or good. Git will stop when there are no commits to be searched and when you end up with the first bad commit – essentially the commit that introduced the bug.
Another aspect of git bisect
is that you can give a sub command to execute that will determine if the state is good or not. For example this could be a command to run an automated test which tells if the bug is present or not. In this way you can automate the whole debugging process.
Logs
Typically when software or a framework crash, it will crash with some sort of a error message which is logged somewhere such as in a file system. So it is vital to know where your logs are located of your application stack. When I say stack, it includes from operating system where your application running on to the sub-tools that your application depends on. Also knowing which log to refer to is also vital, as some crashes might be specific to be registered in a specific log.
The next this is you will need to know how to query the logs. For production, you might be using an external monitoring service such as sentry.io or Logstash. These platforms would come with their own way to query logs, so you would need to know it.
On the other hand, for local development your application would be setup to use local logging mechanism. This could be to use the file system, or even to display the error on screen (with a stack trace). An operating system such as Ubuntu as specific log manager for services also. If any of the services that you rely on doesn’t work, this could be a place to look for logs also. You can use journalctl to query system logs for this. Just verify how your logs are configured your application stack and see where to look.
For querying log files, I would consider running a simple grep
command which the pattern you are looking for. Else I would use tail -f
to watch the log file for changes and re-trigger the bug again and hopefully to the see the bug stack trace in the log.
Reading the stack trace
The stack trace
is a list that contains the code positions from where the code execution jumped/or navigated from. Usually when an exception occurs, the log will also include a stack trace. A stack trace is important as you can determine if the stack trace has an unusual path or to determine what place the error has caused so that you can address that situation better.
On a live debugging session, you will have current stack trace available. You can navigate through this stack trace by simply pointing to a location in the track trace. At the same time, you can see the variables available at the stack frame you selected and their value. This is really handy if you end up with a value that is not-expected and want to determine how the value changed.
Using a Profiler
Sometimes your bug might be related to performance issues. You can profile your application and determine where the performance issues in your application during a period. You simply start the profiler then perform the task in the application and stop the profiler. Then you can analyse the results from the profiler. The results will typically include the duration it took to perform some tasks. This will give an indication where the issue resides or at least help you.
What about print statements?
After considering the previously mentioned techniques for debugging, I hope you are still not thinking to use “print” statements for debugging. Mainly because the tools such as a debugger does the job much better than a print statement.
However you might end up with an application where you don’t have any other tool except to use print statements to debug. Well only in this case it would make sense to use print statements.
So you know, using print statements for debugging is called “Caveman Debugging” for a reason.