Debugging without panic
Debugging is not intuition. It's a search problem, and search has a method — which is why the calmest person in the room is usually the fastest.
- You have a six-step loop that turns "it's broken" into a fix you can prove.
- You can find the commit that introduced a bug without reading any of the code in between.
The instinct under pressure is to change things and re-run: comment out a line, add a log, try a different library version. Sometimes it works, which is the problem — it teaches you to keep guessing. The reliable alternative is slower for the first ten minutes and much faster overall.
7.1The loop
- Reproduce it. A failing test or a saved input that triggers the bug every time. No reproduction means no fix — only a hypothesis.
- Localise it. Halve the problem: which layer, which function, which branch? Logs, breakpoints and binary search on inputs all do the same job.
- Explain it. Write the sentence: "X happens because Y". If you can't, you're still localising.
- Predict a test. Say out loud what a fix should change in the behavior. Then make it.
- Fix the cause, not the symptom. Catching the exception where it surfaced is usually moving the problem, not solving it.
- Prove it. The reproduction now passes, the full suite still passes, and the new test stays in the repo.
7.2Find the commit, not the bug
When something used to work and now doesn't, you don't need to understand the code — you need the commit that broke it. Binary search over history does that in a handful of tests, even across hundreds of commits.
Each click runs the test suite once at that commit. Binary search finds the answer in about log₂(n) tests — six commits take three, a thousand take ten.
7.3Tools that shorten the search
| Tool | Use it when | What it buys you |
|---|---|---|
| Structured logs with a request ID | The bug is only in production | One thread through every step, without reproducing locally |
| Breakpoint + watch | You can run it locally | The real object graph instead of your mental model of it |
| Conditional breakpoint | The bug happens on one input in 10,000 | Stop exactly on the case, not the first 9,999 |
git bisect | It used to work, now it doesn't | The guilty commit, in log(n) tests |
git log -S "string" | A line appeared or vanished | The commit that introduced that exact text |
| Rubber duck | You're stuck and have stared too long | Explaining it out loud exposes the assumption you never checked |
Every bug fix ends with a test that fails without the fix and passes with it. That single habit converts each incident from a story you tell into a guardrail you keep — and it is the cheapest form of documentation your project will ever have.
Changing five things at once and keeping the combination that seems to work. You don't know which change mattered, whether you removed the cause or hid it, or what you broke elsewhere. One change at a time, one hypothesis at a time, with a reproduction you can re-run.
7.4What makes debugging slow
| Habit | What it costs | Instead |
|---|---|---|
| Changing several things at once | The bug disappears and nobody knows which change did it, so it returns | One change, one run, one recorded result |
| Trusting the line number in the stack trace | Optimised code and async boundaries point at the wrong line | Trust the call chain and the values, not the exact line |
| Reproducing by memory | A different sequence each attempt, so the bug appears to be intermittent | A script or a test that reproduces it the same way every time |
| Debugging in production first | Slow feedback, real users, and no snapshot of the failing state | Capture the inputs, reproduce locally, then confirm in production once |
| Fixing the symptom | A try/catch that hides the error and moves the failure somewhere worse | Find the cause, then decide deliberately whether to handle or fail loudly |
The pattern behind all five is impatience with the reproduction. A bug you can reproduce in one command is a bug you will fix today; a bug you can only describe is a bug you will negotiate with for a week.
A bug appears only for one customer. What is the first move?
60-second recap
- Reproduce, localise, explain, fix, prove. Panic is just skipping steps.
- Halve the problem, not the code — one change at a time with a hypothesis.
- Bisect history when it used to work; find the commit, not the entire bug.
- Every fix leaves a test behind. That's how a codebase gets safer over time.