Loud bugs are cheap, a stack trace is a gift. The expensive ones produce no error at all: they return success, render something plausible, exit zero, and quietly do the wrong thing. Learn to recognise their shapes and you find them in minutes instead of days.
The concept
Debugging feels like intuition and is actually a method. The method matters most precisely when intuition fails, when you've tried three fixes and none of them worked.
The core discipline: form a hypothesis that could be wrong, then test it. Most bad debugging is a sequence of plausible changes with no hypothesis attached, which is indistinguishable from guessing.
⭐ The six shapes of expensive bugs
Almost every bug that costs more than an hour is one of these. Learning to recognise them is the highest-leverage skill in this chapter.
Shape 1, Silent success. Something failed and reported success. No exception, no non-zero exit, no log. The most common and most expensive shape. → Countermeasure: every operation that can partially fail must assert its own post-condition and fail loudly.
Shape 2, The helpful fallback. A defensive default turned a failure into plausible wrong behaviour. Every one was written with good intentions. → Countermeasure: fallbacks are fine; silent fallbacks are not. Log, count, or surface every one. Never use a fallback around money, identity, or permission.
Shape 3, The layer below yours. A platform, framework, or runtime did something after your code, without telling you. → Countermeasure: ask "what runs after me?" and "what's common across all the failing cases?"
Shape 4, One variable, two meanings. A value that answers two different questions works until they diverge. → Countermeasure: model facts, not positions (Chapter 20). If a name could be a count or an index, split it.
Shape 5, The untested rare path. Code that runs once, on a path your development environment never hits, reinstall, second device, expired state, empty account. → Countermeasure: enumerate rare paths explicitly and test each on a clean install.
Shape 6, Systematic bias at scale. Fine in every individual case, catastrophic in aggregate. → Countermeasure: measure distributions, not samples.
The method
1. REPRODUCE RELIABLY
If you can't reproduce it, you can't verify a fix.
Narrow to the smallest case that still fails.
2. PROVE YOUR CHANGE IS RUNNING ⭐ do this before anything else
Visible marker → confirm → then debug. (Chapter 28)
3. READ THE ERROR. Actually read it. All of it. Including the stack.
4. WHAT CHANGED?
git log · dependency updates · config · data · someone else's deploy
5. BISECT
git bisect, or comment out half. Halve the search space each time.
6. FORM A HYPOTHESIS THAT COULD BE WRONG
"I believe X because Y. If true, Z would also be true."
Then test Z.
7. ⭐ AFTER THREE FAILED FIXES: STOP.
Your MODEL is wrong, not your implementation.
Do not generate a fourth variation.
8. ASK THE UNLOCKING QUESTIONS
· What is COMMON across all failing cases?
· What runs AFTER my code?
· Is this evidence of failure, or absence of evidence?
· What does this variable actually MEAN?
· What reads this back, and when?
9. FIX THE CAUSE, NOT THE SYMPTOM. Ask "why" three times.
10. RE-RUN EVERY CHECK. Fixes create new bug classes.
11. COMMENT THE FIX EMPHATICALLY if it's non-obvious.
12. LOOK FOR OTHER INSTANCES OF THE SAME SHAPE.
Absence of evidence is evidence
A subtle and repeatedly useful idea. Some observations tell you something precisely because of what's missing:
| Observation | Naive reading | What it actually means |
|---|---|---|
| Email in Sent, not in inbox, not in spam | "Spam filter" | Never accepted, rejected upstream |
| Exit code 0, nothing happened | "It worked" | Pattern matched nothing |
| No log line at all | "It didn't run" | Or it ran and the logger failed |
| No error, wrong output | "Logic bug" | Often a silent fallback |
Learn to read absences. They're often more diagnostic than the presence of an error.
Bug reports that are actually useful
From users or teammates, insist on:
- What you did, exact steps
- What you expected
- What happened
- A screen recording ⭐
- Device, OS, app version, network
Screen recordings are the highest-value QA artifact available. They carry timing, gesture, and the exact failure frame, everything prose loses. "It's stuttering" is undiagnosable; "it's stuttering" plus a video is a bug report.
Production debugging
You can't attach a debugger. You need, in order of value:
- Structured logs with a request/session id threading through
- Error tracking with stack traces and context (Chapter 38)
- Session replay for frontend
- Feature flags to disable a suspect path without a deploy
- Distributed tracing once you have multiple services
Log at boundaries, every external call, every state transition, every failure branch. Include enough context to reconstruct what happened without the user.
📐 Best practice
Reproduce before fixing.
Prove your change is running.
Bisect rather than reasoning about large search spaces.
After three failed fixes, question the model.
Ask "what's common across the failures?"
Fix causes; ask why three times.
Comment non-obvious fixes emphatically, someone will "simplify" them back into the bug.
Re-run all checks after fixing.
Hunt for other instances of the same shape.
Instrument failures as events, not silent catches.
Keep a bug journal. Patterns emerge that individual fixes hide.
💀 Common mistakes
Thrashing. Repeatedly changing things without a hypothesis.
Fixing the symptom. The bug returns wearing a different hat.
Not reproducing first. You have no way to know if you fixed it.
Debugging code that isn't running.
Ignoring the actual error text.
Assuming your dependencies are correct, and assuming they're wrong. Test, don't assume.
Changing several things at once. Now you don't know which fixed it.
Not checking what changed recently. Most bugs are recent.
Silent catches. try {} catch {} with an empty body is how bugs become invisible.
Fixing forward only. New code is safe; the same pattern still exists in five older places.
The professional workflow
The method above, as a checklist you can run when you're stuck and not thinking clearly, which is exactly when you need it:
REPRODUCE smallest case that still fails
PROVE your change is actually running ⭐ first, always
READ the whole error, including the stack
DIFF what changed? code · deps · config · data
BISECT halve the search space, don't reason across it
HYPOTHESISE "I believe X. If true, Z is also true." Test Z.
─────────────────────────────────────────────────────────────
AFTER 3 FAILED FIXES → STOP. The model is wrong, not the code.
─────────────────────────────────────────────────────────────
REFRAME what's COMMON across failures?
what runs AFTER my code?
is this failure, or ABSENCE of evidence?
what does this variable actually MEAN?
what reads this back, and WHEN?
FIX THE CAUSE ask "why" three times
RE-VERIFY re-run every check; fixes create new bug classes
COMMENT emphatically, if the fix is non-obvious
SWEEP find other instances of the same shape
The horizontal rule is the important part. Most time lost to debugging is spent below it, generating a fourth, fifth and sixth variation of a fix that was never going to work, because all of them were correct given a model that was wrong.
Tools, websites & costs
| Need | Tool | Cost |
|---|---|---|
| Error tracking | Sentry, Rollbar, Crashlytics | Free tiers |
| Session replay | PostHog, Highlight, LogRocket | Free tiers |
| Structured logging | Pino, Better Stack, Axiom | Free tiers |
| Feature flags | PostHog, Flagsmith, Unleash | Free tiers |
| Tracing | OpenTelemetry + any backend | $0 + backend |
| Bisecting | git bisect | $0 |
| Mobile inspection | Safari Web Inspector, Chrome DevTools, Flipper | $0 |
| Network inspection | Proxyman, Charles, DevTools | $0-69 |
| Bug reports | Built-in screen recorders | $0, the best one |
| API replay | Hookdeck, Webhook.site | Free tiers |
Alternatives & trade-offs
Print debugging vs a debugger. Debuggers are better for complex state; print/log is better for timing, async, and production. Both are legitimate; print debugging is not unprofessional.
Local reproduction vs production observation. Local is faster to iterate; some bugs only exist with production data, scale, or configuration. Invest in observability so you can debug where the bug actually is.
Fix now vs record and batch. Fixing every bug immediately destroys focus. Batching risks never fixing them. Critical and security: now. Everything else: a tracked list with dates (Chapter 30).
Defensive coding vs failing fast. Defensive code keeps things running and hides problems. Failing fast surfaces bugs immediately and can crash for users. Fail fast internally; degrade gracefully at the edges, and always log the degradation.
Checklist
- I reproduced it reliably before fixing
- I proved my change is running
- I read the full error
- I checked what changed recently
- I have a falsifiable hypothesis
- After three failed fixes I questioned the model
- I asked what's common across the failing cases
- I asked what runs after my code
- I fixed the cause, having asked why three times
- I re-ran every check afterwards
- Non-obvious fixes are commented emphatically
- I looked for other instances of the same shape
- Failures are instrumented, not silently caught
📓 Case Study: nine expensive bugs, eight with no error message
Project: SOLIS. Going back through every bug that cost more than an hour produced an uncomfortable and useful statistic: eight of the nine produced no error message at all. None crashed. All of them "worked."
The catalogue, by shape:
| # | Bug | Shape | Cost |
|---|---|---|---|
| 1 | Screens opened at the previous screen's scroll position | Layer below | 2 days |
| 2 | Paywall granted every user free access | Silent success + fallback | weeks live |
| 3 | Build reported success over a failed build, installed stale binary | Silent success | corrupts all debugging |
| 4 | Cleanup regex matched nothing; 263 MB accumulated | Silent success | near-disaster |
| 5 | 39 images silently showed a placeholder | Helpful fallback | ~1 day, twice |
| 6 | Emails sent, never delivered, not in spam | Layer below | an evening |
| 7 | Completion marks on the wrong item | One variable, two meanings | survived 22 days |
| 8 | Correct answer in position 2 in 294/300 questions | Systematic bias | would have shipped |
| 9 | Sync uploaded but never downloaded | Untested rare path | a week of false confidence |
Bug 1, the two-day one, and the reframe that solved it.
Five reasonable fixes failed in sequence: scroll-to-top after render, the same inside an animation frame, setting the container's scroll position, a temporary CSS lock, and all of them combined.
Then the question that cracked it:
"check if we are using something common for all the lesson, issue must be there"
The common thing was the document itself. The platform's WebView silently restored the document's native scroll offset when content was swapped, after the JavaScript ran, with no event to hook. You cannot win that by trying harder in your own layer; the fix was architectural (move scrolling into an inner container, so the platform behaviour doesn't apply).
Five reasonable fixes failing is the signal that your model is wrong. Each was correct given a wrong model.
Bug 6, absence of evidence, read correctly. Confirmation emails appeared in Sent and never arrived. Two rounds were spent on the obvious suspect (permissions), wrongly. Then:
"its there in sent, but its not there in spam or any other folder in reciver side? (is it autodeleting or something?)"
Not in spam is the tell. Spam means delivered-and-filed. Absent from spam means never accepted, the receiving server discarded it upstream, because the domain had no sending authentication published (Chapter 35). The instinct in that message was exactly right.
Bug 4, silent success in its purest form. A cleanup script's pattern used syntax that means something different on the platform it ran on. It matched nothing, no error, no output, exit code 0. Files accumulated in a tracked directory until they were 263 MB and one commit from permanent history.
The fix wasn't just correcting the pattern; it was counting what remained afterwards and failing loudly if it wasn't zero.
The pattern that emerged. "Silent success" appeared four separate times in 24 days, in four unrelated subsystems, a cleanup script, a build pipeline, an asset loader, and a payment stub. Nobody connected them until the bugs were catalogued together.
📐 That's the argument for keeping a bug journal. Individual fixes hide patterns. A list of them reveals that you have one recurring architectural weakness, not nine unrelated incidents, and fixing the class is far cheaper than fixing nine instances.
The unlocking questions, in the order they earned their keep:
- "What's common across the failing cases?", solved bug 1
- "Is this evidence of failure, or absence of evidence?", solved bugs 6 and 4
- "What does this variable actually mean?", solved bug 7
- "What reads this back, and when?", would have prevented bug 9
- "What runs after my code?", bugs 1, 3, 6
⚠️ Deviation: fixes were applied forward but rarely swept backward.
When the "silent fallback" pattern was understood in one place, the other instances weren't hunted down. Bug 5's shape, a graceful fallback hiding a failure, had already occurred once before with a different asset set and hadn't been generalised.
Fixing forward is natural. Sweeping backward for the same shape is the discipline, and it's where most of the leverage is.
Lessons
- ⭐ The expensive bugs are silent. Eight of nine produced no error. Optimise your defences for silence, not crashes.
- Six shapes cover almost everything. Silent success, helpful fallback, the layer below, one-variable-two-meanings, untested rare path, systematic bias.
- "Silent success" is the dominant shape. Four instances in 24 days across unrelated subsystems.
- After three failed fixes, your model is wrong. Stop generating variations.
- "What's common across the failures?" is the highest-yield question in debugging.
- Absence of evidence is evidence. Not-in-spam and exit-0 each mean something specific.
- One variable answering two questions is a bug with a delivery date.
- The rare path is the untested path. Reinstall, second device, expired, empty.
- Comment hard-won fixes emphatically, or they get simplified back into the bug.
- Keep a bug journal. Nine incidents were really one recurring weakness.
- Sweep backward. Fixing forward leaves the same shape in five older places.