Three-Step Debugging: A Methodical Approach

I've been working as a developer for almost twenty years now. In that time I have had the pleasure of helping many developers, and helping them debug code. Some were new to the field, had vastly more experience than me.

While some are naturally somewhat gifted at debugging, it's important to understand that the skill can be learned. In fact, even very experienced developers can struggle with it

I've known developers who stare at their code trying to reason their way through it. I've known developers who will change things at random until the problem goes away. I've known developers whose technique I describe as "I've tried nothing and I'm all out of ideas".

I take a very structured approach, and this is something I've meant to write down for years now. Solving 90% of the of the problems you will face comes down to three simple steps.

Identify Outputs

Identify Inputs

Repeat

Identify Outputs

There is a bug in your code; that much we know. It's causing something to not work as expected. The first step in debugging is to identify what that something is.

Determine what your expected output is, and what the actual output is.

Walk through the call stack to find where the output diverged from expected.

Identify Inputs

This step is crucial and often the most challenging.

In an ideal world we'd only be debug pure functional code. The only inputs would be parameters, and all calls within are deterministic.

We don't live in an ideal world.

We often find ourselves debugging code that inherits values and state from elsewhere. This can be self-fulfilling, such code is usually hardest to test and most prone to errors.

Sources of state include, but are not limited to:

Members of the containing class or its parents

Non-deterministic values and methods

The current time

Random number generators

Potentially flakey network services

Databases

File systems

User input

Global variables

Environment variables

Configuration files

This list is far from exhaustive and meant to give you an idea of the kinds of things you might need to consider.

While debugging, it's important to identify and note all these inputs. We then need to understand their state at the time of the error. This is often where I see developers struggle. Some part of their code can vary and they have failed to identify it.

Ways to identify their state logging, or by using a debugger to inspect the state of the system at the time of the error.

It comes down to a is a matter of personal preference as well as comfort with the tooling.

I know my way around my debugger, and yet I more often than not am inclined to just spit the state to standard out. I find this to be the most effective way of debugging. I know developers who are very good at using debuggers. I also know developers who spend a lot of time trying to make sense of their debugger.

I've never met a developer who is bad at using print statements.

The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.

– Brian Kernighan

Repeat

After identifying the code's outputs and inputs, use this information to track down the problem. You will need to repeat this process recursively through the call tree.

We work our way through the call stack, identifying the unexpected outputs and inputs as we go. Keep this up and we will locate the root cause.

Other Thoughts

Identifying outputs and inputs isn't limited to values of variables. It's about the state of the system as a whole - including where boundaries get crossed.

You need to understand how the system handles crossing boundaries and ensure that the handoff results as expected. Understanding how the system manages crossing these boundaries is crucial for identifying where errors might occur.

You also may need to understand the state of the larger system at the time of the error. "The system" is not limited to the code you're working on. It can also include the state of the database, the state of the file system, the state of the network, cookies, sessions, the end user's browser, etc.

When you've identified the issue, write a test that reproduces it. This confirms you've fixed it and understood it, as well as preventing its recurrence. If you don't already have a strong test suite, regression tests are a great place to start.

Conclusion

I hope this guide helps you improve your debugging skills. Debugging is a skill that you can learn and improve over time.

Debugging is not just about fixing errors. It's an opportunity to learn your codebase and how to enhance it.

And remember, careful thought, combined with well-placed print statements, remains the most effective debugging tool.