A Debugging Method That Works
Reproduce, isolate, understand, fix, prove โ replacing superstitious guesswork with a loop that always terminates.
Debugging is not a talent; it's a search procedure. The difference between a struggling junior and a calm senior is that the senior's loop halves the search space every step.
The five-step loop
- Reproduce reliably. A bug you can't reproduce on demand is a bug you can't fix. Find the minimal input: which exact data, which exact sequence? (An automated reproduction โ a failing test โ is gold: it becomes your regression test later.)
- Isolate. Binary search the pipeline. Does the data arrive wrong from the API, or get corrupted in processing, or render wrong? Log/inspect at the midpoint. Wrong upstream? The bug is upstream; throw away the downstream half. Repeat. Even a 10-step pipeline falls in ~4 steps.
- Understand. Found the line โ now explain why it produces the wrong output. "The filter keeps items where
qtyis truthy, and 0 is falsy" is understanding. "I removed the filter and it worked" is superstition; it'll bite you next week. - Fix the cause, not the symptom. Guarding against the corrupted value downstream hides the bug; fixing the producer of the corrupted value removes it. Symptom patches accumulate into systems nobody can reason about.
- Prove it. Write the failing test FIRST (it fails before the fix), then fix, then watch it pass, then keep it forever as a regression test. A fix without a test is a rumor.
Tools that matter
- Breakpoints > console.log. The debugger pauses state in context: every variable in scope, the call stack that led here. Log statements show you a photo; the debugger gives you the whole album, backward.
- Conditional breakpoints for loops that iterate 10,000 times: break only when
i === 937. - Watch expressions track a value across steps.
- Network tab: what actually left the browser, headers included. Half of "frontend bugs" are requests the code never made, or responses it misread.
git bisect(module 6!): when "it used to work," history finds the culprit commit in log-time.
Reading stack traces like a pro
Read the top frame first โ that's where the error was thrown. Then walk down to the first frame that is your code (skip library internals). The error message tells you what; the frame tells you where; the surrounding code tells you why. undefined is not a function means a name resolved to undefined โ check for typos, missing imports, or an object that isn't the shape you assumed.
When you're stuck
Explain the bug out loud, line by line, to anyone (or a rubber duck) โ articulating the expected vs actual flow frequently reveals the gap mid-sentence. And if 30 minutes of binary search hasn't narrowed anything: you're searching the wrong pipeline. Stop, re-map the data flow, pick a new midpoint.