I’ve wanted to write a post about debugging for a while, but it’s a topic that can be really difficult to discuss in a general way. Approaches to debugging vary wildly and span multiple technologies and domains (hardware vs. software vs. other).
Bugs can also exist on all types of scales—everything from users reporting an issue in your production application, to code failing to compile, to your app crashing when you use it in a certain way.
Despite the wide topic breadth, I want to share some thoughts and approaches that have helped me debug several software projects. These thoughts will be most applicable toward mobile and web development, though much of the content can likely be applied elsewhere.
Depending on the problem at hand, I usually choose from several approaches to understand and correct it:
Rubber ducking, or talking the issue through with someone else, is one of the first things I do when I encounter a problem. Exposing an issue to multiple eyes is usually enough to illuminate and squash the bug pretty quickly.
This works well when you are on a team or have access to someone with enough context to understand the issue you are facing. It doesn’t work so well if you need niche expertise, or knowledge is siloed on your team.
Writing a failing test
A great way to understand a bug is to write a test that reproduces it. This allows you to reproduce the bug in a constrained manner, and it prevents it from re-appearing in the future.
This approach works best when the bug is something that can be captured by a unit test. Sometimes this isn’t possible–for example, if a bug is specific to a certain environment (e.g. device or OS version), or if it requires specific characteristics that only exist in production (e.g., a table with millions of rows).
Looking at logs
Can you see the issue manifesting itself in your logs? If not, it may be worth updating them to gain more insight into what might be happening. This works best when you have a good logging system in place. It obviously won’t work for issues like compilation errors, or if you are working on an embedded or IoT-type device with limited access to logs.
Using space reduction
Space reduction is my term for deleting or commenting large sections of the codebase in order to narrow down the problem area. You can also do this by leveraging your version control system and checking out a different branch, or stashing some uncommitted changes. It’s similar to using a tool like git-bisect, which I discuss later.
This approach works great for compilation errors or recently-introduced bugs. It doesn’t work so well if the feedback loop is slow (e.g., you have codebase with extremely long compilation times).
Stepping through the code
This can be a nice way to slowly trace what may be causing your issue. It can work well if you have access to a good debugger, but it won’t work well for environments that are heavily asynchronous or make heavy use of callbacks. It also won’t work if the issue you are investigating is related to timing or a product of a race condition.
Most of the above strategies can be supplemented with good debugging tools. Depending on the domain and environment you are working in, the tools will vary.
This approach is nice because your version control software is orthogonal to your project’s language and technology. git bisect can help you search your Git commit history to determine when a bug was introduced (e.g., which commit).
I’ve found this tool is most effective when you keep a clean commit history. It works by binary searching through commits and asking you to verify whether or not the bug exists in each commit.
It’s generally helpful to have an easy way to test each commit (usually a unit test). If you have a commit history where your code doesn’t compile or tests don’t pass for certain commits, this won’t work well.
If you’re writing client-side browser code, there is probably an arsenal of browser plugins that can give you more visibility into your application. This is certainly true for the big SPA frameworks (React, Angular, Vue, etc.). However, it obviously won’t work if you are writing backend or mobile code.
Simulators or emulators
I’ve booted up the iOS simulator several times to help debug web bugs that only manifested themselves on mobile devices. Most browsers give you some type of “responsive” mode, but sometimes, it’s important to drop down to the simulator or emulator level to reproduce the issue and solve it.
Having recently worked on a large website project, I’ve found that cloning production or staging databases to your local environment can be extremely helpful. Being able to configure your local environment with the click of a button is essential for most web projects.
Sometimes, you can’t figure out an issue on your own, or within your team. Perhaps it’s an issue with a library, an external dependency, or the integration you are using.
In these scenarios, I’ve had good luck with the following approaches.
For open-source projects hosted on GitHub (or similar), I like the project issues tracker. I’m often able to find an issue that describes the problem I’m trying to solve. If not, I may open up a new issue (if appropriate).
This approach can be a bit latent, as not all libraries are actively maintained or have someone monitoring issues. You may have to wait for other folks to read and respond to your submission.
Every developer uses Stack Overflow to some capacity. However, I’m willing to bet that few developers actually post to Stack Overflow when trying to root-cause and resolve issues.
You need to be selective in what you post, and make sure it’s appropriate, but I’ve gotten pretty good mileage out of this approach. Sometimes, I end up solving my issue and I answer my own question. In that scenario, there is a record of my challenge and solution for the next developer who gets stuck on a similar problem.
What are your favorite debugging strategies and tools?