Debug Strategy
Adopt a reproducible method (bisect, isolate, hypothesis) instead of trial-and-error tweaking.
Why
Random tweaks — commenting lines, swapping values, restarting the server — feel productive but rarely find the root cause. They mask the bug by coincidence (it disappears on a retry) or move it elsewhere (the symptom changes but the cause remains). A reproducible method turns debugging from superstition into engineering: you form a hypothesis, design an experiment that can falsify it, and keep a log so you never re-investigate the same dead end. The single biggest time-saver is git bisect — it finds the exact commit that introduced the bug in log(N) steps instead of reading diffs. Teams without a method spend 3x longer on hard bugs and often 'fix' them in a way that regresses two weeks later.
When
Before the first hard bug. Takes 30 min to learn the method, saves hours per bug. Skip never.
How
1. Reproduce reliably — a bug you can't reproduce is one you can't fix - Write a failing test that triggers it; if you can't, narrow the steps until you can - Record the exact env (node version, env vars, DB state) so it's not "works on my machine" 2. Form a hypothesis and write it down before acting - "The null ref happens because the user was created before the profile migration ran" - A written hypothesis is falsifiable; a vague hunch is not 3. Isolate the variable — change one thing at a time - Disable half the middleware; if the bug persists, the cause is in the other half - Use git bisect to find the introducing commit: git bisect start && git bisect bad HEAD && git bisect good v1.0 4. Inspect, don't assume — read the actual values - console.log / debugger at the boundary, not deep inside - Check the DB row, the network response, the env var — the bug is usually where the data crosses a boundary 5. Fix the cause, not the symptom — then verify the test passes and stays green - Add a regression test so this bug can never return - If the fix is "add a null check", ask why the value was null and fix that
Example
# Reproduce: a failing test that triggers the bug
test("user created before profile migration does not crash", () => {
const user = factory.user({ createdAt: "2024-01-01", profileId: null });
expect(() => renderProfile(user)).not.toThrow();
});
# Bisect to find the introducing commit
git bisect start
git bisect bad HEAD # current is broken
git bisect good v1.4.0 # v1.4.0 was fine
# git checks out commits; you mark good/bad; it narrows in ~7 steps for 100 commits
# Hypothesis log (keep a DEBUG.md while investigating)
# 2024-03-15 14:30 H1: null profileId — FALSIFIED, guard exists since v1.2
# 2024-03-15 14:45 H2: race between user.create and profile.create — CONFIRMEDCommon Problems
- ⚠Changing 3 things at once, the bug disappears, and you have no idea which change fixed it — so it regresses later.
- ⚠Debugging in dev with hot reload, where the bug doesn't reproduce because the module state is stale — you waste an hour on a phantom.
- ⚠Reading the code instead of the runtime values — the bug is almost always in what the code actually does, not what it appears to say.
- ⚠Skipping the regression test, so the same bug is re-introduced in a refactor 3 months later.
Best Practices
- ✓Keep a DEBUG.md per investigation: timestamp, hypothesis, experiment, result — it turns a 4-hour hunt into a 30-minute read when the bug returns.
- ✓Use git bisect early; it's free and finds the exact commit in logarithmic time, no reading required.
- ✓Add a regression test in the same PR as the fix so the link is permanent and reviewable.
- ✓When the fix is a guard (null check, try/catch), spend 10 more minutes finding why the bad value reached the guard — the guard hides the real bug.