Skip to content
RungsySign in

Systematic Debugging

Reproduce, bisect, hypothesise, verify — a method that beats staring at code every time.

35 minDifficulty 2/5craft · metaAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.

Before this

Why this exists

Staring at code that "should work" and reading it over and over rarely finds the bug — you already believe it's correct, which is exactly why you wrote it that way. Systematic debugging replaces belief with evidence: reproduce the failure reliably, narrow down where it starts, form a specific hypothesis, and test that hypothesis directly instead of guessing.

The mental model

Debugging is binary search over a space of possible causes, not a search over code. Each check you run should roughly halve the remaining space of "where could this be" — not add another random print statement in the same function you've already stared at ten times.

How it works

Reproduce first, or you're debugging a ghost

A bug you can't reliably trigger is a bug you can't verify you've fixed. Before touching code, find the smallest, most reliable set of steps that reproduces the failure — even if that means writing a script that hammers an endpoint 1,000 times to catch an intermittent race.

Bisect the space, don't scan it linearly

If a regression appeared somewhere in the last 40 commits, `git bisect` finds it in about six tests via binary search, not forty. The same principle applies inside a function: check a value at the midpoint of a suspect code path before checking every line from the top.

State the hypothesis before you check it

"I think `userId` is undefined by the time it reaches this function" is a falsifiable claim you can test with one log statement or breakpoint. "Let me just look around and see" is not a hypothesis, and it's why unstructured debugging sessions run long — there's no way to know when you're done because there was never a specific claim to disprove.

The bug is almost never where you first suspect

The function that's throwing is usually a victim, not a culprit — it received bad data from somewhere upstream and is failing correctly. Walk backwards along the data's path, checking assumptions at each hop, rather than adding defensive code at the crash site that just moves the symptom somewhere quieter.

The mechanism

The loop is deliberate: reproduce, narrow, hypothesise, test — and if the hypothesis is wrong, you form a new one with the information you just gained, rather than starting over. Once a fix is applied, you re-run the ORIGINAL reproduction, not a new simplified case, because that's the only proof the actual reported bug is gone.

flowchart TD
  A[Reproduce reliably] --> B[Bisect: narrow the location]
  B --> C[Form a specific hypothesis]
  C --> D{Test the hypothesis}
  D -->|Confirmed| E[Fix at the root cause]
  D -->|Refuted| C
  E --> F[Re-run the original reproduction]
  F -->|Still fails| B
  F -->|Passes| G[Done]
Diagram source for Systematic Debugging.

What people get wrong

Adding more log statements everywhere will eventually reveal the bug.
Untargeted logging produces so much noise that the signal gets lost — a single, well-placed check at the midpoint of the suspect range is more informative than fifty scattered prints. Shotgun debugging feels productive because you're doing something, but it doesn't shrink the hypothesis space the way a targeted binary search does.
If the fix makes the error message go away, the bug is fixed.
The error message going away often just means the symptom moved somewhere quieter — the actual root cause, like a null value from an upstream function, may still exist and surface differently later. Fixing symptoms instead of causes is why the same underlying bug reappears in a slightly different form a few weeks later, wasting the debugging effort a second time.
Debugging is a talent some people have and others don't.
Debugging is a method — reproduce, bisect, hypothesise, verify — that improves with deliberate practice like any other skill, not an innate trait. Treating it as talent discourages people from learning the systematic process, and encourages randomly poking at code instead, which is genuinely the slower approach regardless of experience.

When not to use it

The bug is a crash with a full stack trace pointing at an exact line.
Skip the bisection step — you already have the location. Go straight to forming a hypothesis about why that line fails and checking the values involved.
The failure only happens in production and cannot be reproduced locally at all.
Reach for observability data — logs, traces, metrics — from the actual failing request rather than trying to guess a local reproduction; see `logging-observability`.

Terms

Reproduction case
The smallest, most reliable set of steps known to trigger a bug, used as the ground truth for whether a fix actually worked.
Bisection
Repeatedly halving the space of possible causes — in code, in commit history, or in a data pipeline — rather than checking candidates one at a time from the start.
Root cause
The earliest point in a chain of events where something went wrong, as distinct from the point where the failure became visible.
Heisenbug
A bug that seems to disappear or change behaviour when you try to observe it, typically caused by timing sensitivity that debugging tools themselves perturb.

In an interview

Walk through how you'd debug an intermittent test failure that only fails about 1 in 20 runs in CI.

  • first make it reproduce more reliably, e.g. by running it in a loop locally
  • suspect timing or ordering issues given the intermittency
  • form a specific hypothesis about a race condition or shared state rather than guessing broadly

Can you recall it?

Why is stating a specific, falsifiable hypothesis better than "just looking around" when debugging?

Keep track of this

Add Working Practice to your map and Rungsy will schedule reviews so you actually remember it.