Watching an experienced developer find a bug looks like intuition. It is not. It is a search executed in a particular order, and the order is learnable.
The loop
- Reproduce it. Reliably, on demand. If you cannot make it happen, you cannot confirm you fixed it.
- Narrow it. Cut the space in half, then in half again.
- Read what is actually there. Not what you meant to write.
- Form one hypothesis. Predict what you will see if it is true.
- Test that one thing. Change one variable.
- Fix, then verify the fix reproduces nothing.
Most wasted debugging time is spent skipping step 1 or violating step 5.
Reproduce first
"It sometimes fails" is not a reproduction. Keep cutting until you have the smallest input that fails every time.
Common hidden variables behind an intermittent bug: something time-dependent, something order-dependent, a cached value, a leftover record from a previous run, or two things racing.
Narrow by halving
The single most effective technique, and the most underused.
If the bug is somewhere in 200 lines, put one check in the middle. Correct at that point? The problem is after it. Wrong already? Before it. Seven checks locate a line in a codebase of a hundred thousand.
The same works across time. If it worked last month and fails now, bisect the commits: test the midpoint, discard half the history, repeat. Git even has a command that automates it.
The most expensive mistake
Changing several things at once. It feels efficient and it destroys your information: when the behaviour changes you no longer know which change did it, and if two changes had opposite effects you learn nothing at all.
One change. Observe. Then the next.
The second most expensive
Reading what you meant instead of what you wrote. Your eyes fill in the intended code. This is why saying the code aloud, line by line, works so unreasonably well, and why explaining the problem to someone often solves it before they answer.