Let’s be honest, bugs (and debugging) are a painful, time-consuming and often frustrating process. However, it is part of our lives as developers, and also one of the best ways to learn and improve.

Sometimes I get asked by other developers for help on how to track and fix a bug and, while we do some pair programming, I try to show them the process I usually follow when debugging.

It’s important to understand the different strategies and tools we can use while debugging to make this a more streamlined and efficient process.

Understand the problem

It might sound quite obvious and even trite, but you will be surprised at how often developers try to skip this part and jump right into the issue. Before you start trying to fix something, you need to make sure you understand exactly what you’re dealing with, and can achieve that by reading and studying the errors.

Reading error messages is usually the first thing to do; they can give you really good hints or even point out the place where the bug is happening. Errors can be in any form, but ideally, they would be descriptive enough to be useful during debugging.

Unfortunately, that’s not always the case. I remember once I was doing a college project consisting of building a linux distro from scratch, the process was tedious, and we needed to follow a file with tons of pages to do the setup. At some point during the process, the app would just throw an “Error 2” that was not documented anywhere (not even on the web), and you had to restart the whole process from the beginning.

make: *** [install] Error 2

Whenever you face something like that, ideally there will be a stacktrace attached to the error. Unfortunately, stacktraces are not really helpful in understanding the problem, but sometimes they can point out where in the process the issue is happening, or at least lead you close to it.

> node app.js

/app.js:10 <-- Last step before issue happened
  undefinedFunction();
  ^

ReferenceError: undefinedFunction is not defined <-- The error
   at thirdFunction (/app.js:10:3) <-- File, line and column
   at secondFunction (/app.js:6:3)
   at firstFunction (/app.js:2:3)
   at Object.<anonymous> (app.js:13:1) <-- Less helpful lines
   ...

Sometimes you have no messages, no codes, no stacktraces. If you understand the problem now or have nothing to work with, the next step is to reproduce the error.

Reproduce the error

Once you understand or at least have an idea of the problem, the next step is to try to reproduce it. 

To do this, you want to go to the person that reported the issue or the description (if they documented it), and see if there are specific steps they followed when they saw the error. The key here is to collect as much information as possible.

Something I like to suggest to testers, is to record the session so you can follow what they did step by step. It’s also good to keep developer tools like network tab and console open while debugging web applications so everything gets recorded and it’s easier to identify.

It’s important to be able to reproduce the error multiple times. This will give you a better comprehension of the problem. If you’re dealing with a consistent bug or an edge case, you can decide how to approach the fix based on that.

Isolate the code

By this point, you should have a better idea of where the bug is coming from. Notice how we haven’t looked at the code until now. It’s easier to look at it once you understand the problem.

An easy yet super useful strategy is to comment out code lines or blocks until you find what’s causing the bug. You have two options here: 

One, comment everything out and start uncommenting until the error happens again. This is my preferred option unless I’m super confident about where the bug is. However, depending on the code size, it can be time consuming. 

init() {
   // Error stopped when running only this line
   this.functionOne();
   //this.functionTwo(); ----
   //this.functionThree();  | --> Bug can be anywhere here
   //this.functionFour(); ---
 }

Two, if you’re feeling confident (or lucky), you can comment out the line(s) you think the bug is at and see if the error stops.

init() {
   this.functionOne();
   this.functionTwo();
   //this.functionThree(); <-- I feel the bug can be here
   this.functionFour();
 }

Since you already know how to reproduce the bug, you just need to start removing some lines of code and running the steps to replicate until the error stops appearing. 

Write some tests

Last but not least, we need to make sure that the issue has been resolved and that it won’t happen again.

The best way to do this is by adding the scenario to the test cases. If you want to fix it first and then add the tests or if you want to do some Test-Driven Development and write tests before the fix, is up to you.

After this, you will feel more confident about the fix and you will increase the tests coverage, so it is a win-win.

Debugging tools

No matter what your debugging process is, there are some tools you can use and combine to make your life easier. Let’s talk about some of them.

Logs

Logs are an essential part of software development. They offer detailed information about the process execution that we can use to analyze and identify the source of the issue.

Logs are specially useful for reproducing errors, since you get access to all the events that happened that led to the error.

It’s also important to make sure we are logging useful information. Excessive logging can make it heavy, hard to navigate and understand, and adds unnecessary noise.

Printing

Depending on the language I’m working with, this is one of my preferred debugging methods. I use it with golang all the time.

It’s as simple as adding some console prints in specific places in the code. I usually add some prefix to the messages so I can search them fast in the console. I use it mostly on two cases:

Following the execution path, so that we know a specific piece of code is being called or not.

fmt.Println(“####### I am on method X”)

Getting the value of a property at a specific moment.

fmt.Println(“####### Value of X is %s”, X)

The downside of this method is that you need to rebuild or rerun the program if you want to change something you are printing.

Debuggers

Debuggers are a powerful tool. They allow you to go through the code line by line, giving you full control to decide when to move to the next part of the code.

One of the main benefits of debuggers is that you can inspect the state of the program at any point during its execution.

In a certain way, debuggers are similar to printing. You use them to know the program execution path and state. Of course, debuggers are way more powerful, and I prefer them over printing when possible.

Rubberducking

Rubber Duck Debugging (Rubberducking) is a technique that consists of explaining your code to a rubber duck or any other object. You can gain a better perspective while verbalizing your thoughts. It sounds simple and yet so effective.

If you don’t feel comfortable talking to a rubber duck, or your wife is starting to think you are getting crazy, something I like to do is to explain it to another person, developer or not.

I know some of you might say that sounds like pair programming, but you will see that sometimes as soon as you start explaining the issue to the other person, you realize what the problem is without them saying a word.

And if it ends in being a whole conversation and a pair programming session, that is totally fine. It is another really good tool for debugging when you have tried everything without getting anywhere, and you just need a fresh pair of eyes.

Final thoughts

Having a streamlined process that combines different techniques, can make it easier to track bugs, find better solutions and save you a lot of time by increasing the debugging speed.

There are no techniques or tools better than others, they just fit differently depending on the problem. If you understand the problem, it would be easier to determine which tool(s) to use.

It’s important to be conscious about what we log and what error messages we use while coding, since that can save us time later and make the process easier.

Debugging doesn’t need to be hard. The moment you start seeing it as a way to learn and improve, both the code and your skills, you will start enjoying it.