3.3A strategy for debugging
Last lesson's six steps included "locate the root cause" as if it were one move. In a forty-line program it is. This lesson is about keeping it close to one move as programs grow, because the difference between debuggers-by-method and debuggers-by-suffering is almost entirely how they narrow the search.
Finding by inspection
Your first tool is reading. When the program is small or the change is recent, just look: most fresh bugs live in the line you most recently touched, and the second-best suspect is the line you copy-pasted it from. Inspection works precisely when you can hold the whole suspect region in your head, which is one more reason this course keeps nagging about small functions: they're the unit of holdable.
Inspection's failure mode is familiar to anyone who has proofread their own essay. You don't read your code; you read your intentions, and the bug is by definition where those differ. When ten minutes of staring produces nothing but confidence that "this should work," stop staring. The code is doing something; go watch it do it.
Finding by running
The empirical method: form a hypothesis about where things go wrong, make the program reveal what's actually happening at that point, and let the evidence split the territory. Concretely you need three ingredients.
A reproduction. Step 1's gift keeps giving: every narrowing experiment below starts with "run the failing case again," so a one-command reproduction pays rent on every iteration. Keep the failing input small if you can; if a 900-line input file triggers the bug, spend a few minutes finding the 3-line version that still does.
A way to see inside. Evidence means observing values and control flow mid-run. The next two lessons are tooling for exactly this (prints and dbg! in 3.4, the debugger in 3.5); for today's lesson, assume you can check any intermediate value.
A halving instinct. Here's the strategy that separates an hour from an afternoon. Don't inspect forty lines one by one; check a value in the middle of the suspect span. If it's already wrong there, the bug is in the first half; if it's still right, the second. Repeat. Each check halves the territory, and seven checks can corner a bug in a hundred-line span. You'll meet this idea again as an algorithm with a name in lesson 7.10's number-guessing game, where you'll be the computer playing it.
The shape it usually takes in practice is checking at the seams. Programs built per chapter 2 are pipelines of functions:
fn main() {
let data = load();
let score = evaluate(data);
report(score);
}
The user says the report is wrong. Three suspects, two seams. Check data at the first seam: looks right, so load is innocent. Check score at the second: wrong, and the search just collapsed to evaluate. Two observations, one function left, and its interior can be halved the same way. This is the everyday rhythm of debugging done well: wrong report → wrong score → wrong line, each step eliminating half the world.
Comment things out. The blunt-but-effective cousin (lesson 1.2 promised it would matter): disable a chunk and rerun. Symptom gone? The chunk was involved. Symptom unchanged? Probably innocent; restore and disable elsewhere. It's halving with a hatchet, ideal for "which of these five steps is even involved?" questions. One discipline: re-enable as you go. A program with six forgotten commented-out lines is a new and exciting bug of its own.
Best practice
Change one thing per experiment, rerun, record what you learned, undo. The undo matters as much as the change: narrowing experiments that pile up turn the program into a haystack of your own probes, and at the end you can't tell which change "fixed" it. (Version control, when you adopt it, makes this discipline mechanical: it shows you every probe still in the code.)
Key insight
Every technique in this lesson is the same idea wearing different clothes: make the invisible visible, then cut the search space, preferably in half. Hypothesis, observation, elimination. Debugging is the scientific method practiced on your own beliefs, with the program as the experiment that never lies.
Quiz time
Question #1
A four-stage pipeline (read → clean → total → print) produces a wrong total. You check the value between clean and total: it's correct. Where's the bug, and what did that one check buy you?
Show solution
In total or print (and print is usually a quick inspect, so realistically total). One observation eliminated half the pipeline: read and clean are cleared.
Question #2
Why does "find a smaller failing input" count as debugging progress, before you've located anything?
Show solution
Every later experiment reruns the reproduction, so a smaller case makes each iteration faster to run and to reason about: less data means fewer places the wrongness can hide, which is itself a form of narrowing.
Question #3
You commented out the bonus-calculation block and the crash vanished. Your colleague says "great, ship it without bonuses." What's the polite version of your objection?
Show solution
The experiment located the bug's neighborhood; it didn't fix anything. The crash lives somewhere in (or is triggered by) that block, which now needs steps 3 through 6: understand, plan, fix, retest, with the block restored. Also, payroll would like a word.
Time to arm the strategy. Next lesson: the three printing tools that make program internals visible, including one designed for exactly this job.