Debugging is a reading skill

Most bugs are found by reading carefully, not by running more code.

The instinct, when something breaks, is to run it again. Add a print. Add five more. Change a line and see what happens. It feels like progress because the terminal keeps producing output, and output feels like information.

Most of the time it isn’t. You’re generating noise around a question you haven’t asked yet.

The bugs I’ve actually solved were solved by reading. Not skimming for the suspicious-looking line; reading the way you’d read a contract, one clause at a time, asking what each one commits you to.

The code says what it says

Every bug is a gap between what you believe the code does and what it does. Running it tells you the outcome. Reading it tells you where the belief went wrong, which is the only part that transfers to the next bug.

So I start at the thing that broke and walk backwards, out loud, in plain sentences. This function gets called with a document. It reads a field off the parent. The parent is fetched by name. Where does the name come from? Four steps back, there’s a line that assumes the parent always exists, and a code path where it doesn’t.

No amount of printing would have found that faster than reading found it. The print statements would have shown me a null, and I’d have added a guard, and the bug would have moved somewhere else.

Reading is slower and that’s the point

The uncomfortable part is that reading feels unproductive. Twenty minutes in, you have nothing to show. No commit, no output, no visible motion. Meanwhile the run-and-poke approach produces a change every ninety seconds, none of them right.

But a fix you can’t explain isn’t a fix; it’s a coincidence you’ve committed to a branch. If you can’t say why the code did what it did, you can’t say your change stops it, and you certainly can’t say it doesn’t break the other four callers.

What to read

Not everything. The skill is in choosing.

The call site tells you what the author expected to be true. The tests tell you what they were worried about, and the absence of a test is louder than any comment. Git blame tells you whether a line is load-bearing or leftover. A function with three arguments and one caller is usually a refactor someone abandoned halfway.

And read the surrounding code, not only the broken function. Bugs cluster around boundaries: where a value crosses from one layer to another, where a type gets loosened, where someone wrote a helper to paper over something awkward. The line that crashed is rarely the line that was wrong.

The tell

Here’s how I know I’m reading instead of guessing. Reading produces a prediction: if my understanding is right, then this input should produce this specific wrong output. Then I run it, once, to check.

Guessing produces the opposite: a change, then a hope, then a run to find out. If I catch myself running the code to learn what it does rather than to confirm what I already believe, I stop and go back to reading. That’s the whole discipline.

It’s also why debugging gets faster over years while typing speed stays flat. You’re not getting quicker at the loop. You’re needing fewer trips through it.