How to Debug
Debugging is not guessing and poking until the error goes away. It’s a method: reproduce it, observe what’s actually happening, form one hypothesis, test one variable, and narrow until the cause is cornered. Done well it’s calm and fast. Done badly, changing random things and hoping, it’s slow, and it “fixes” bugs that come right back because you never found the cause.
This is the general loop for any bug. When the bug is specifically about speed, How to Debug Performance adds the profiling and query-level tools on top of this same method.
Reproduce It First#
What’s expected: Before you try to fix anything, you can make the bug happen on demand.
A reliable reproduction is half the fix. Until you can trigger the bug at will, you can’t tell whether a change fixed it or the bug just didn’t show up this time, so you’re guessing. Nail down the exact conditions: which input, which user, which state, which sequence. If it only happens in production, use the tools that see production, session replay to watch what the user actually did, traces and logs to see what the system did. Often, building the repro reveals the cause on its own.
If it’s intermittent, you haven’t found the real trigger yet. “Sometimes” usually means “always, when some condition you haven’t spotted is true”, concurrency, a specific data shape, a cache state. Hunt that condition; it is the bug.
Read Before You Change#
What’s expected: You’ve read the actual error, trace, and logs before editing a single line.
The instinct under pressure is to start changing code. Resist it. The system is usually telling you what’s wrong, in the stack trace, the error message, the trace showing which call failed, the logs showing the last thing that happened. Read those first. A minute reading the trace beats an hour editing the wrong file. Most “mysterious” bugs stop being mysterious once someone actually reads the error instead of skimming it.
Narrow by Halving#
What’s expected: Each step you take roughly halves the space the bug could be hiding in.
The core move is binary search: don’t scan the whole system, cut it in half and ask which half holds the bug. Then halve again. The space you’re halving can be anything:
- The code path: is the data already wrong when it enters this function, or does this function corrupt it? One check tells you which half to keep.
- Time:
git bisectfinds the exact commit that introduced a regression by halving the history, far faster than reading every diff. - The input: strip the failing input down. Does half of it still fail? Keep halving until you have the smallest thing that breaks it, which usually names the cause.
Ten halvings narrow a thousand possibilities to one. Linear poking never gets there.
One Variable at a Time#
What’s expected: You change one thing, observe, and keep or revert it, before changing the next.
When you change five things at once and the bug goes away, you’ve learned nothing, you don’t know which one mattered, or whether you added two new bugs that cancel out. Change one variable, observe the effect, decide, then move on. This is the scientific method, and it’s the same discipline as making one change in one place: a controlled change teaches you something; a shotgun blast of changes teaches you nothing and leaves a mess.
Try to disprove your hypothesis, not confirm it. “I think it’s the cache, so let me find where the cache would cause exactly this” is how you fool yourself. “If it’s the cache, then clearing it should fix it, and it didn’t” is how you learn. The bug is very often precisely where you were certain it couldn’t be.
Fix the Cause, Not the Symptom#
What’s expected: Your fix addresses what generated the bug, and a test makes sure it can’t come back.
Once you’ve found it, resist patching the symptom where it surfaced. A null on the checkout page is a symptom; the cause is wherever that value was allowed to go missing. Patch the page and it reappears on the next page that uses the value; fix the source and it’s gone everywhere (this is root cause vs. symptoms, at the code level). Then write the test that reproduces the bug, so the next change can’t quietly bring it back. A bug fixed without a test is a bug scheduled to return.
When You’re Stuck#
- Check the dumb things. Is it deployed? Right environment? Stale cache? A surprising share of “impossible” bugs are one of these.
- Question an assumption. You’re stuck because something you’re sure is true isn’t. List what you’re assuming and verify one.
- Explain it out loud. Describing the problem to a colleague, or a rubber duck, forces the gap in your reasoning into the open. You’ll often solve it mid-sentence.
- Step away. Ten minutes off the screen beats an hour of tunnel vision. The answer arrives when you stop gripping it.
Related: How to Debug Performance, Observability, Session Replay, Thinking Like an Engineer