5 Steps to a Bullet-Proof Debugging Strategy
“Can you take a quick look at this bug?”
These are words you hear most weeks as a software developer.
But does that question fill you with dread? Or does it excite your inner detective?
The words “software bug” may bring out a cold sweat or get you eager to learn; which reaction those words provoke often lies with whether you have a good debugging strategy.
Lack of a debugging strategy is a common problem. Creating a strategy is a good idea, and it’s even better to create an approach to debugging that works regardless of external factors, like which company you work with or the complexity of the bug.
So here’s what I’ve learned over the years from the people I respect about how to fix even the trickiest of software bugs. These methods helped me to fix bugs both faster and easier, so feel free to steal them!
A quick note on the order: This method in this order is what works for me. You may not need some of these stages, and you may combine others. Find what works best for you.
#1: Get into the Debugging Mindset
There are a lot of unknowns when you’re first presented with a software bug. So many variables. And usually, so little time.
And the unknown can be overwhelming.
One thing I noticed among my peers was a calm, focused, and inquisitive approach to a software bug. Not panic and finger-pointing, which can be the go-to state for people who may not understand the nature of code. (For example, non-technical team members.)
A calm attitude will help others to be reassured a fix is on its way and create a collaborative atmosphere of “let’s get this problem fixed ASAP!”
Another benefit of emulating this calm approach is that it makes me less tempted to code a hack for the fix, and instead take the time to code a permanent solution. Although it’s tempting to take shortcuts when pressure mounts, it’s not worth it.
Bug fixes that are just hacks create layers of brittle code, which in turn causes “code rot.” Code rot is the degradation of your code structure so it can no longer support any more changes. David Thomas and Andrew Hunt speak about the important concept of strengthening your code base in their book “The Pragmatic Programmer,” which I highly recommend you read. They reinforce that where there is code rot, there will inevitably be even more bugs. So, instead of fixing your bugs thoroughly, and in turn building more stable software, you are in fact creating more problems for your future self.
An inquisitive approach is all about not taking your bugs personally. Treat them as learning opportunities, not as defects in yourself.
If your peers don’t have that calm, inquisitive mindset, they will be much more prone to finger-pointing, which creates a poor work environment. If you are a leader (or aspire to become one), try fostering a “blameless culture” that helps your team to get into this calm mindset easily—without the fear of being berated.
Developer evangelist Jason Hand also promotes the blameless culture, saying that it makes sure no one hides vital information and the team can learn quickly from mistakes.
A debugging mindset is one of calm and focus. Rest assured that your debugging strategy is robust enough to handle even the most elusive bugs.
Approach bugs as you would any area of your life—armed with the right information and with a calm and panic-free attitude.
#2: Prioritize Which Bug to Fix
When it comes to consequences, by far the most important metric to factor is how many users are affected by this bug. Not error count.
The reason for this thinking is that your user experience is paramount. If you have poor performing software for users, they will leave and you will have lost the lifeblood and purpose of your software—customers!
So, if you have 10,000 errors that affect one customer, that’s not as bad as 500 errors affecting 250 customers.
I learned early on that not all bugs are created equal, and prioritization is the best way to cut through the noise. Therefore, I tend to organize bugs by weighing each against the likelihood of it occurring versus the consequence of it occurring.
To help sort out the nitty-gritty, and keep my users front of mind, I often refer to my friend, the risk assessment matrix.
The risk assessment matrix helps me to list bugs in order of priority quickly. If the frequency is high, and the severity of the impact is major for my end users, that’s where the priority lies. Easy!
If bugs land in the same priority bucket, usually a set of three questions will help me figure out which one to start with:
- Given the time and understanding I have, am I the best person to tackle this bug, or is one of my peers?
- Are there bugs on the list that are causing other bugs on the list?
- Does resolving a particular bug downgrade the severity of others?
A crash reporting tool like Raygun helps here, as you can tag and sort errors, which helps you find out the volume of errors being generated by specific criteria.
#3: So Where Is the Bug, Exactly?
The first port of call to pinpoint a bug’s location is finding out which part of the application the error occurred in.
Locating the part of the application not only tells me where to start, but also helps me recreate the user actions leading up to the bug.
Ask yourself smart questions as you go. One trick is to keep the text editor open on another screen and throw the questions in there as they come up.
Usually, the questions I ask fall into two categories:
Question 1: When does the bug happen?
Getting a hold of the stack trace isn’t usually too much of a problem for me, as I use crash reporting software, which gives me a real leg up when I’m trying to understand the specific user actions leading to the bug.
Question 2: Can I reproduce the bug over and over again reliably?
I need to understand if the bug requires a particular set of data for it to be replicated. Again, using crash reporting software helps me here, as it gives me the stack trace. If you don’t have the stack trace, it can be time consuming for your quality assurance team to replicate the bug, eating up valuable project time.
Since most bugs are just edge cases (something special that you have to handle) that are unaccounted for, I find it most useful to know what information was passing through the system at the time of the crash. Then I can determine a “start point” for testing. Try to get the timing as close to the bug as possible—it will shorten the process a little.
My next step to getting a bug cornered is to identify the classes and methods that will relate to the part of the application where the bug occurred.
I can now use my debugging tools to add breakpoints and pause the execution of my application as close to the bug as possible—a tip that’s saved me so much time!
Now I have the bug cornered. Almost. Next, I need to weed it out …
#4: Which Line of Code Is the Bug On, Exactly?
In order to fix the error properly, you must find the line of code the error occurred on.
In an ideal world, I would be able to “step over” my code (executing my code line by line) to see the error occur.
Not possible? For example, you may not have a debug build. In this case, you would want to log information out to a text file.
Another thing to consider is that the noise of system events in the log can cause your log statements to get lost. A nifty trick my chief technology officer taught me was to prefix my log statements with a unique identifier.
I find using my initials followed by the pipe symbol is an easy way. A quick “find all” for this prefix will help me find all of my statements. It is then only a few keystrokes to copy all my log statements into a new text file for me to review without the noise:
Console.WriteLine(“MD | My method was called”);
The next step I take is to understand which responsibilities that class or method has.
If the code is undocumented, I usually write comments in plain English above portions of code to help me understand what it intended to accomplish.
But what if the portions of code are significant? Well, in this case, I’ll do a quick Unified Modeling Language diagram to see the key dependencies. UML diagrams are also handy for finding the relationships between classes.
Once you find the line of code, the scope of the fix will also present itself. As in: Will this take me five minutes, or five days to fix?!
Is the bug in your method implementations? Or will it involve rewriting class structure? If you already work in a team, make a brief estimate to take to your team lead, who will be able to allocate the correct time and resources.
But don’t spend too much time here. It’s easy to get lost gathering information and trying to understand data—you may already have everything you need for the next step.
#5: What’s the Solution?
The trouble with software bugs is that each one is different.
But if you have gone through each of the above stages of the debugging strategy (or some iteration of the stages), you will have a clearer idea of the best fix.
If you have tried everything and nothing is working the way it should (which is often the case, I think), ask for help. Something that’s helped me a great deal in my career as a software developer has been learning how to ask for help the right way.
As in not looking completely lost (even if you are). Try to be as prepared as possible to ask a good question. (To be clear, this tip is about how best to pose questions to your peers, rather than to yourself, as we talked about in the previous section.)
I found a great example from Hartley Brody, who writes about debugging for beginners in this article. He says that starting with well-thought-out questions to your peers is pivotal to a good outcome. He writes that an excellent question structure has the following four components:
- Explain what you’re trying to do
- Show the code that’s giving the error
- Show the entire stack trace including the error message
- Explain two or three things that you’ve tried already and why they didn’t work
I will add that thanking people appropriately goes a long way, and if you see people asking questions that you can answer, reciprocate and offer solutions.
And if you are too shy to ask questions, you can always check out John’s guide to mastering your soft skills.
How Does Your Debugging Strategy Stack Up?
These are the stages I go through to find and fix all sorts of software bugs. This workflow is ideal for solving any bugs, from minor bugs to the trickiest critters deep in the code base.
I designed my current debugging strategy to be effective and to keep my stress levels down. This strategy works because I am able to take on bugs as learning opportunities, rather than allowing them to overwhelm me.
Every bug I fix helps me grow as a developer, both in accumulating knowledge about my code base and in developing soft skills like time management. Of course, this strategy will change and grow as I do.
Ultimately, a robust debugging strategy saves me time so I can continue doing what I enjoy the most—coding!