The Art of Debugging: Or How I Learned to Stop Worrying and Love the Stack Trace
Debugging is half detective work, half therapy session. Here's how to get good at both.
The Art of Debugging: Or How I Learned to Stop Worrying and Love the Stack Trace
There is a particular kind of silence that falls over a developer's desk at 11pm. Not the comfortable silence of a good book or a sleeping house. This is the silence of a bug that should not exist, a system that was working fine ten minutes ago, and a git diff that looks completely innocent.
We have all been there. And if you haven't, you are either very new or very lucky — and your luck is coming.
Debugging is, at its heart, detective work. And like all good detective work, it rewards patience, clear thinking, and the willingness to question your most cherished assumptions — especially the one that begins with "there's no way that could be the problem."
Start with the Crime Scene
The first rule of debugging: read the error message. All of it. This sounds obvious, but you would be amazed how many developers — senior ones included — skim the first line and start Googling before they've absorbed what the stack trace is actually telling them.
The stack trace is a witness account. It is telling you exactly what was happening the moment things went wrong, in reverse chronological order. The bottom of the stack is where execution started; the top is where it fell over. Read it like a story, because it is one.
Once you've read the error, reproduce it reliably. A bug you can reproduce consistently is halfway solved. A bug you can only reproduce sometimes is a ghost story — technically real, but hard to reason about. Before you write a single line of fix code, you want a simple, repeatable case that demonstrates the problem. Ideally, something you can run in under ten seconds.
The Hypothesis Loop
Good debuggers are good scientists. They form hypotheses, test them, and update their model of the world based on what they find. The key word there is test — not "assume the fix worked" and ship it.
The loop looks like this:
- Observe the failure
- Form a hypothesis about the cause
- Predict what you'd see if the hypothesis were true
- Run a test (a print statement, a debugger, a unit test)
- Compare the result to your prediction
- Update your model and repeat
The temptation is to skip straight from step 2 to a code change. Resist this. You are not trying to make the symptom go away — you are trying to understand why it exists. Fix the cause, not the symptom, and you never see that bug again.
AI as Your Rubber Duck
The rubber duck debugging technique has been around for decades: explain your problem out loud to an inanimate object (traditionally, a rubber duck), and in the act of explaining, you often find the answer yourself. The duck doesn't help. You help yourself by being forced to articulate the problem.
AI has made this dramatically more powerful. Instead of a rubber duck that silently stares at you, you now have a rubber duck that asks questions, spots patterns across millions of codebases, and occasionally says "have you checked your timezone handling?" at exactly the right moment.
Modern AI coding assistants excel at debugging in a few specific ways. They can look at an error message and a code snippet and surface related issues they've seen in similar contexts. They can walk through your logic out loud if you paste it in and ask "does this look right?" And they are infinitely patient — they will not sigh audibly when you ask the same question three different ways.
What they're not good at is understanding your system in full. An AI doesn't know that your staging database has a slightly different schema than production, or that your colleague's recent migration had a subtle off-by-one error. That context lives in your head. The most effective AI-assisted debugging is a partnership: you bring the context, the AI brings the pattern matching.
The Nuclear Option: Binary Search
When you have no idea where the bug lives — the error is vague, the codebase is large, or the problem only shows up at the end of a long chain of operations — binary search debugging is your friend.
Comment out half the code. Does the problem persist? Then it's in the half that's still running. Restore, comment out a different half. Repeat until you've isolated the offending section to something small enough to reason about.
This feels crude. It is crude. It is also extremely effective, and experienced developers use it without shame.
Git bisect does this automatically for regressions: you tell it which commit was good, which commit is bad, and it binary-searches through your commit history to find the exact commit that introduced the problem. If you write atomic, well-described commits, this is one of the most powerful tools in the shed.
The Most Important Debugging Skill
After all the tools and techniques, the most important thing you can develop is the willingness to be wrong.
Most bugs exist because of a mistaken assumption. The code does what it says; it just isn't saying what you thought. When you're stuck, the question to ask is: "what am I assuming to be true that might not be?" Your understanding of the framework. The order of operations. What that function actually returns.
When you find the bug — and you will find it — take a moment to understand why your mental model was wrong. That understanding is the real product of debugging. The fix takes thirty seconds. The updated understanding stays with you for years.
The stack trace isn't your enemy. It's the crime scene photograph that breaks the case. Learn to read it well, and debugging becomes less of an ordeal and more of a puzzle. A frustrating puzzle, yes. But there's a particular satisfaction in solving it that no other part of software development quite matches.
Jako Heiberg
Software developer with 40+ years of building things that work. Full-stack, FastAPI, React. Based in Cape Town, working remotely, worldwide.