22

Debugging by Hypothesis, Not Guessing

The four-step loop that turns a mystery into a solved problem — every time, without random thrashing

Guessing loops feel like work, but they're not debugging

When something breaks, most people do this: change something, run it, see if it's fixed. Change something else. Run it. Repeat until it accidentally works. Then move on.

This is not debugging. It's random search with occasional lucky hits — and it has a predictable outcome: the bug "disappears" without you knowing why, and returns three weeks later in a slightly different form.

The two costs of guessing

First, it's slow — each random change costs a full run cycle and gives you almost no information. Second, even when it works, it costs you understanding. You don't know what you fixed, so you can't prevent the next occurrence. Systematic debugging is faster and builds a mental model that pays forward.

The alternative is the scientific method, applied to code. It's four steps, and once you internalize it, you can't un-see it.

The loop: Reproduce → Hypothesize → Test one variable → Observe

Four steps. Run them in order every time. Skipping step one or step two is where the flailing starts.

1
Reproduce it on demand

Before touching a single line, make the bug happen deliberately. If you can't reproduce it, you have no target. A reproduction also tells you the exact conditions required — which is half the diagnosis.

2
Write down your hypothesis

State, in plain language, what you believe is causing the bug and why. "I think X happens because Y." Do this before running anything. A written hypothesis forces clarity and gives you a falsifiable target.

3
Test exactly one variable

Change one thing that would confirm or deny your hypothesis. Not two things. One. If you change three things and the bug goes away, you've learned nothing — and you've introduced changes you don't understand.

4
Observe what actually happened

Read the output precisely — not what you expected, but what it says. If your hypothesis was wrong, use the new information to form a better one. Loop back to step 2. Never skip straight to another change.

The loop ends when you have a confirmed fix

A fix is confirmed when: (a) the bug no longer reproduces with the change applied, AND (b) the bug still reproduces when you revert the change. Both checks. One direction only is incomplete evidence.

What makes a hypothesis testable

A good debugging hypothesis has three parts: a proposed cause, a predicted effect, and a test you can run right now. If any of those three are missing, the hypothesis isn't ready.

Weak hypothesis:
"Something is wrong with the
 authentication."

Strong hypothesis:
"The session token is expiring
 before the refresh call fires
 because the token TTL is 3600s
 but the refresh interval is 4000s.
 Test: set both to 60s and watch
 whether the error appears at 60s."
The weak version is a topic, not a hypothesis. It doesn't predict what you'll observe, and it doesn't tell you what to change.
The strong version names a specific mechanism, predicts when the failure occurs, and specifies a test that changes exactly one variable (the TTL/interval ratio).
The test at the end is what makes it falsifiable. If you can't write the test, the hypothesis needs more refinement.

The act of writing the hypothesis often reveals what you're missing. "I think the bug is in the database query" is a starting point, not a hypothesis. What specifically about the query? Under what conditions? Filling in those blanks is where the insight comes from.

Rubber duck the hypothesis before you test it

Explain the bug and your hypothesis out loud to someone — or to a rubber duck, or to Claude. Forcing language on a vague intuition often breaks it open. You'll frequently catch a logical flaw in the explanation before you ever run a test.

Bisection: the fastest way to narrow a search space

When you don't know where in the code the bug lives, bisection is the fastest tool. The principle: divide the problem space in half, test which half contains the bug, discard the clean half, repeat. In the worst case, you find it in log₂(N) steps.

git bisect

The bug appeared somewhere in the last 200 commits. git bisect start, mark the known-bad commit and the last-known-good commit, then test the midpoint. Git finds the introducing commit in about 8 steps instead of 200.

Comment out halves

Inside a function or script: comment out the second half, test. If the bug is gone, it was in the second half — restore it, comment out the third quarter. Each step cuts the search space in half.

Log at midpoints

Add a log statement at the halfway point between "where the code starts" and "where the bad output appears." If the log fires with a good value, the bug is downstream. If it fires with a bad value or not at all, it's upstream.

Minimal reproduction

Strip the repro case down to the smallest possible input and environment that still shows the bug. Every stripped-away piece is one less thing that can be the cause. What remains is the suspect set.

Bisection works on anything ordered: commits, lines of code, input data, configuration keys, network hops. The moment you can say "the bug is in this half, not that half," you've halved the problem.

Where Claude Code fits — and where it doesn't

Claude Code is useful at specific points in the debugging loop. Used correctly, it accelerates the process. Used carelessly, it becomes a random-change generator with better prose.

Use
Observation: reading error messages and stack traces

Paste the exact error output and ask what it means mechanically. "What does this stack trace tell me about where execution failed?" This helps you form a hypothesis — it does not replace forming one.

Use
Hypothesis refinement: explaining mechanisms

"Here's my hypothesis. Does this mechanism match how [this library/protocol/system] actually works?" Claude can correct a factually wrong assumption about how something behaves, which improves your hypothesis before you waste a test cycle.

Avoid
Blind fix attempts: "just try this"

Asking "fix this bug" without going through the loop first turns the AI into a random-change generator. It produces confident-sounding guesses. You apply them without understanding. The bug recurs in a different form. You're back to guessing.

Avoid
Substituting AI confidence for a reproduction

If you can't reproduce the bug yourself, Claude can't debug it either — it can only guess more fluently. "I can't reproduce it but Claude explained it" is not a closed bug. Reproduce first, always.

The principle: you own the loop, AI accelerates steps in it

Hand the AI a specific job at a specific point in the loop — observe this output, explain this mechanism, check this hypothesis for logical errors. Never hand it the whole loop. The loop is yours. Your hypothesis, your experiment, your confirmation. The AI is a tool inside the loop, not a replacement for it.

Loop check

Three debugging situations. Pick the move that follows the loop.

Scenario

A user reports intermittent failures in a form submission. You look at the code and immediately spot what looks like an obvious race condition. The fix seems clear.

What do you do first?

Scenario

You have a failing test. You've formed three hypotheses: a wrong environment variable, a stale dependency, and an incorrect function argument. You can change all three in two minutes.

How do you proceed?

Scenario

You're looking at a long stack trace and an error message you don't fully understand. You want to use Claude Code to move faster.

Best use of the AI here?

The next time something breaks

Before touching any code: write down what you observe, write down your hypothesis in one sentence, pick one test. Run the loop once deliberately. That single disciplined pass will teach you more than an hour of random changes — and it will get faster every time you do it.