Watch a junior and a senior developer face the same mysterious bug and the difference is rarely typing speed or memorised APIs. The junior starts changing things — a flag here, a reorder there — hoping the error evaporates. The senior does something that looks slower and finishes faster: they stop, form a theory about what the system is actually doing, and design the smallest possible test of that theory. Debugging is not a search through code; it is a search through hypotheses.
The method underneath is old, boring and astonishingly reliable — it is the scientific method wearing a hoodie. Observe precisely, hypothesise narrowly, test cheaply, repeat. Every debugging war story that ends well is this loop, run with discipline, under pressure.
Reproduce first, or you are debugging fog
The first discipline is refusing to theorise about a bug you cannot reproduce. A reliable reproduction — even an ugly one, even one that takes ninety seconds of clicking — converts an anecdote into an experiment. Until then you are debugging witness testimony. Shrink the reproduction ruthlessly: half the input, half the steps, half the config. Every halving that still shows the bug cuts the suspect list roughly in half too.
Intermittent bugs deserve the same treatment with different tools: logging around the suspected region, capturing the failing state, running the flaky test a thousand times in a loop overnight. 'Cannot reproduce' usually means 'have not yet controlled the variable that matters' — time, concurrency, data shape, environment. The variable you cannot control is very often the answer itself.
Binary search beats staring
The single most transferable debugging technique is bisection. If the code worked last Tuesday and fails today, the bug entered in a known window — and version control can walk it: test the midpoint commit, discard the clean half, repeat. Tools like git bisect automate this into a dozen checkouts even across thousands of commits. The same halving works in space as well as time: comment out half the pipeline, stub half the modules, feed half the data. Ask 'is the bug in this half?' instead of 'where is the bug?'.
Bisection's quiet superpower is that it requires no understanding of the bug at all — only a reliable way to ask 'is it broken here?'. Understanding arrives at the end, when the search corners the defect in a screenful of change. Staring, by contrast, requires you to already suspect the right place, which is precisely what you don't know yet.
Read the error like it means it
A surprising share of debugging time is lost to not actually reading the error. The message names a file, a line, a type, an expectation — and the eye slides off it toward the code it already suspects. Slow down and parse the words literally: 'undefined is not a function' says something real was undefined at a real moment; a stack trace is a map with an X on it. When the message seems absurd — 'that file definitely exists' — the absurdity is information: some assumption in your model of the system (working directory, environment, version, cache) is false, and the bug lives in that gap.
The final discipline is closing the loop. When the fix lands, ask why the bug was possible, whether its siblings exist elsewhere, and what test would have caught it — then write that test. A bug fixed without a lesson extracted is a bug scheduled for re-release. The developers people call wizards are mostly people who never pay for the same lesson twice.