A method for debugging, for when you have run out of ideas
Most debugging advice is a list of tools. Tools help when you know where to point them. This is about the other situation: the bug has resisted a day of effort, every hypothesis was wrong, and you are now changing things at random and hoping.
Here is the procedure I fall back on. It is slower than a lucky guess and it always terminates.
1. Make it reproduce on demand
Nothing else works until this does. A bug you can trigger in ten seconds is a bug you will fix; a bug that happens “sometimes, after a while” will consume your week.
Spend real time here. Write the script, add the debug menu item, hard-code the state, whatever it takes. If it only reproduces on a specific device or a specific account, get that device and that account.
If it genuinely will not reproduce, you are not debugging yet — you are collecting data, and the task is to add enough logging to production to make it reproducible later. That is a different job and it is worth admitting you are doing it.
2. Write down what you believe
Literally write it. One sentence: “The list is empty because the API returns an empty array.”
This is the step people skip and it is the one that does the work. An unwritten belief is slippery — you will quietly revise it as evidence arrives and never notice you were wrong. Written down, it is falsifiable.
3. Find the cheapest thing that would prove you wrong
Not the thing that would confirm you. The thing that would disprove you.
For the belief above: print the array’s count at the point it arrives. That is one line and thirty seconds, and it splits the world in half. Either the API returned nothing — and the bug is on the server, and you are now debugging a completely different system — or it did not, and everything you believed about this bug was wrong and you have saved yourself a day of looking at the wrong layer.
4. Bisect
If step 3 does not settle it, stop reasoning and start bisecting. Data flows from A to Z; check at M.
print("M: \(items.count)")
The value is either correct at M or it is not, and either way you have eliminated half the code. Ten iterations of this find a bug in a codebase of a thousand functions, and none of them require you to be clever.
The same applies to time rather than space:
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect is the single most underused tool I know. It turns “when did this break?” from an
archaeology project into twenty minutes of running the app and typing good or bad. If you have a
test that reproduces it, git bisect run does even that part for you.
Tip
Bisecting works on inputs too. If a 4MB JSON file crashes the parser, cut it in half and try again. Six rounds and you have a twelve-line document that reproduces the crash — which is both the diagnosis and the regression test.
5. Change one thing
When you start trying fixes, change exactly one thing and re-test. Two changes at once and a passing test tells you nothing about which one mattered — and one of them may have introduced the next bug.
This is the rule I break most often under time pressure, and it is the one that costs me most when I break it.
6. Say it out loud
If all of that fails, explain the problem from the beginning to another person, or to nobody. Rubber duck debugging is real and the mechanism is not mystical: explaining forces you to state your assumptions in order, and it is impossible to say “and then it fetches the user, which is always non-nil at that point” out loud without immediately wondering whether that is actually true.
Most of the times this has worked for me, I never finished the sentence.
The failure modes to watch for
“That can’t be it.” Every bug lives in the code you were sure was fine. That certainty is not evidence, and it is the reason the bug survived this long — you excluded its location from the search on the first pass.
Fixing the symptom. The list is empty, so you add a guard that shows a placeholder. The list is still wrong, and now it is wrong silently. Before writing the fix, be able to say the sentence “the bug happens because X” — if you cannot, you are patching.
Not reading the error. I have lost hours to a stack trace whose second line said exactly what was wrong. Read all of it, including the frames you assume are boilerplate.
Why bother with a procedure
Because on a good day you do not need one. Intuition works, you see the bug in five minutes, and none of this applies.
The procedure is for the day intuition fails — and on that day, the alternative to a procedure is changing random things for six hours and shipping something that appears to work. I have done that version. The bug always comes back, and it comes back in front of a customer.