"Debugging is like being the detective in a crime movie where you are also the murderer." - Filipe Fortes
It doesn't matter at what point in your career you may be, it's a known fact* that nothing in programming works first time. So when you've written some awesome code that absolutely SHOULD be working, you might despair when you see that error message flash up on the screen. Where do you even begin?!
But do not give up hope, bold text-based adventurers, for with some simple gumshoe guidance you too can be a Debugging Detective!!
Debugging can be made easier by:
- Know which tools are available for your language/platform, and learn how to use them
- Use a dedicated debugger wherever possible
- Find the entrypoint for your problem and start there. Always start with something that you know about the problem
- Eliminate sections of code from your investigations quickly, and narrow down your search to code that may be responsible. Follow your code "down the rabbit hole" to other files/modules/components. Do not make assumptions
- Write unit tests and refactor your code to be "clean", more testable and maintainable. Small methods with one job and a clear name are best
- Minimise your edit/build/debug cycle by reducing build times, and investigating multiple possible places that your bug could be within each of these cycles
- Take your time and don't discount blocks of code until you are confident that they are working as expected
To be a good debugging detective...
The first tip is to use the right tools for the job, and be comfortable knowing how to use those tools. If you're writing a web application, do you know how to use your browser's Dev Tools? For backend server work or console applications, does your IDE have a built in debugger?
There are some situations where it might still be necessary to use
console.log(), but there are myriad tools and development environments available right now, and a built in debugger is always preferable if possible. Using a debugger allows you to pause the execution of your code at "breakpoints" and inspect the values of variables as they stand at that point in time; you can then advance the code one line at a time, step over or step into method calls, or simply continue executing to the next breakpoint.
In simple terms, if there is a built-in debugger available for your language/framework of choice you should probably use it, it'll save you lots of time and effort in the long run - and we all know that ultimately we want to be spending our time adding value to our applications for our customers, rather than firefighting bugs. Everyone is going to have their editor or IDE of choice, but I will take the time to recommend Visual Studio Code, which is an open-source cross-platform editor from Microsoft (yes, really!). Despite the name it actually has nothing to do with "Visual Studio", the slow, clunky, Windows-only IDE of yesteryear. Instead, VSCode (as it's commonly known) is a lightweight editor with a thriving ecosystem of extensions so that you are in control of exactly what functionality you have running in your development environment. It comes with a Node.js debugger built in, and has debuggers available for all manner of languages and frameworks. It even has the concept of "extension packs" which bundle related extensions together so that you can get all of the tools you need from one place.
Some useful extensions and extension packs for debugging include:
Most debuggers are simple to use - you add breakpoints to your code by clicking the margin in your editor alongside the line of code you want to add the breakpoint to, and then when you run your application in "debug" mode in the editor the execution will stop when it reaches that line of code. Knowing how to get the most out of your debugger is crucial to efficiently hunting down those nasty bugs.
In VSCode, if you have the appropriate extension installed for the environment that you wish to debug, it's usually as simple as navigating to the debug view, selecting the config from the drop-down list (or add a new one if it doesn't exist) and then hitting the play button. You'll then get a set of controls appear at the top of the editor pane which allows you to control the progress of the application.
If your language or framework of choice doesn't appear to have a debugger available, or if you need to test functionality that might only be available in the environment to which you're deploying (e.g. connecting to AWS S3, which you might not have access to normally), you may have to resort to printing out statements in your application and running it multiple times to narrow down where the bug might be. In this situation the key here is to try to narrow down the search for your bug as quickly as possible - place statements before and after a lines of code that you think might be the problem and print out the value of variables at those points for inspection, and try to "halve the distance" to where your bug is by ruling out areas of your code quickly. Hopefully your unit tests already cover many of the possible permutations of variable values etc., but if you're stuck you can always check to see what values your method arguments have when you run your application and then write tests to cover those values to see if you can reproduce the issue that way - you can then focus on changing your code to make your test pass, in true Test-Driven-Development style!
To be a good debugging detective...
Sometimes the most difficult part of debugging (or anything for that matter) is knowing where to begin. This will depend on your language and framework/library combination but the idea is to start with something that you know. This may be a particular button that a user is clicking on which then causes an error to occur, or even just a page that they may be looking at (or an area of the page that is either experiencing the issue or they are interacting with). This is your debugging entrypoint, and is where you should start your investigation.
Once you know your entrypoint, find that page/section/button/whatever in your code and start sticking some breakpoints in there - use the error message as a clue, but don't discount anything too quickly, as sometimes errors can be misleading. Debug your application using the tools mentioned in section 1, and inspect what happens as you step through your application. As you do so, try to eliminate parts of your code that appear to be behaving as expected, and narrow down the search. At this point it's more than likely that you will have other modules/files/components that are called by that first section of code, and so you will have to travel "down the rabbit hole", and dive into those as well. Try to stick to the same principles of eliminating code that is working as expected, and isolated possible candidates for where the problem may be. Continue to do this and you will hopefully narrow down your issue to the point where you have a good handle on what is going wrong.
If you're stuck and still struggling to know where to go and what to look at, a good approach again is to try writing some unit tests on your code (if you haven't already - I hope you have!) to ensure that it is working as expected. This may even be a good opportunity to refactor your code to make it more testable and to make it easier to isolate areas of your code that might be difficult to test or debug. Remember that readability and maintainability are generally more important than writing code in as few lines as possible. Small methods that do one job and are easily testable and well named are best - for more info on this topic I HIGHLY recommend "Clean Code" by "Uncle Bob" Martin (Published by Prentice Hall, ISBN 9780132350884).
To be a good debugging detective...
Lastly in our quest to be an awesome Debugging Detective, I recommend trying to minimise feedback time. What do I mean by this? It's important when debugging to try to reduce as much "dead time" as possible. This includes building your application, running it in debug mode (or not, if you can't use a debugger), gaining feedback on any hunches you may have about where the problem lies or what it is, and then making any code changes and repeating the process again.
If you have to build your application from scratch each time, this can add a huge amount of dead time into your debugging process, where each tiny change to the code to gather more and more clues results in waiting, waiting and more waiting. At this point it really is worth seeing if you can do one of a few things to reduce the time it takes to complete this cycle:
1) Reduce the build time
If you are building a fat JAR, can you change your process to just build a regular JAR from your app and provide the dependencies outside of the JAR when debugging? Can you perform incremental builds instead of building the whole thing every time, e.g. in .NET Core as in this article?
2) Try to address as many possibilities as you can in each cycle
If your edit/build/debug cycle is on the long side (maybe you just can't get your build times down), try to investigate as many possibilities as you can in each cycle. For instance, you may think that a problem is down to a particular method or the value of a specific variable, but do you know that? If not, try to keep an open mind and investigate other areas of your code at the same time until you can narrow down the field. This means that a long build will not have to run for each small change to your code, cutting down your overall debugging time.
3) Take your time
This might seem counter-intuitive, but don't jump from breakpoint to breakpoint too quickly and assume certain sections of code are fine - take the time to understand what is happening at each step and ensure that it is exactly what you expect. Write tests around your code to confirm this and then you can confidently eliminate that code from your investigations. It's all to easy to assume that a block of code is fine, skip over it too quickly, spend hours looking at other parts of your code only to find that you didn't investigate the first block of code thoroughly enough. Once again, to re-iterate, write tests to confirm that your code is doing what it should be doing
Hopefully you've found this article helpful, and you feel you're on your way to being an awesome Debugging Detective. Debugging can feel like a dark art at times, but with these tips you should be able to focus your efforts on what is needed to isolate the problem faster and more effectively each time.