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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
"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.
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.
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.
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.
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?
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?
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?
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.