Debugging Methodology
The Iron Law
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
Random fixes waste time and create new bugs. Complete Phase 1 before proposing fixes.
Debugging Directness
- Never speculate about a bug without reading the relevant code first.
- State what you found, where (file:line), and the fix. One pass.
- If the cause is unclear: say so explicitly. Do not guess.
- No preamble, no hedging. Finding first, explanation after.
The Two-Attempt Rule
After 2 failed fix attempts at the same problem, STOP:
| Attempts | Action |
|---|---|
| 1-2 | Normal Phase 3 (hypothesis + test) |
| 3 | STOP. Structured analysis before retrying |
| 3+ no progress | Ask user — root cause unclear or architectural change needed |
Structured analysis before attempt 3:
- Write down what you tried and what happened
- Articulate your assumptions — which one is wrong?
- Identify information gaps
- Re-read relevant code with fresh eyes
- Return to Phase 1 with a new mental model
The Four Phases
Phase 1: Root Cause Investigation
1. Read Error Messages Carefully
- Read stack traces completely; note line numbers, file paths, error codes
- The error message often tells you exactly what's wrong — don't skim
2. Reproduce Consistently
- Can you trigger it reliably? Exact steps? Minimal reproduction?
- If not reproducible, gather more data — don't guess
- For intermittent issues: add logging with timestamps, stress test, look for race conditions
3. Check Recent Changes
git log --oneline -20 # Recent commits
git diff HEAD~5 -- src/ # Recent code changes
git log --all --oneline -- <file> # History of specific file
4. Gather Evidence at Each Layer
For multi-component systems, trace at every boundary:
# Trace data flow through each layer
echo "=== Layer: HTTP Request ===" # What's coming in?
echo "=== Layer: Middleware ===" # What transforms it?
echo "=== Layer: Business Logic ===" # What processes it?
echo "=== Layer: Database ===" # What gets persisted?
echo "=== Layer: Response ===" # What goes out?
5. Trace Data Flow — Where does the bad value originate? Keep tracing upstream until you find the source.
6. Ask "Why Does It Work Locally?"
When bugs appear in production/CI but not locally:
| Factor | Local | Production |
|---|---|---|
| Concurrency | Single user, sequential | Many users, concurrent |
| Data volume | Seed data, small | Real data, large |
| Configuration | Dev defaults | Production settings (pool sizes, timeouts, caches) |
| Network | localhost, fast | Real latency, DNS, proxies |
| Dependencies | Mocked or local | Real services, rate limits |
| Timing | Debugger pauses, slow | Full speed, race conditions |
Map every environmental difference. The bug lives in one of these gaps.
Phase 2: Pattern Analysis
- Find working examples of similar code
- Compare against references — list every difference
- Understand dependencies (settings, config, environment)
Phase 3: Hypothesis and Testing
- Form Single Hypothesis: "I think X is the root cause because Y"
- Test Minimally: Smallest possible change, one variable at a time
- Verify: Worked? → Phase 4. Didn't? → NEW hypothesis (don't add more fixes)
Phase 4: Implementation
- Create Failing Test — simplest possible reproduction, automated
- Implement Single Fix — ONE change at a time
- Verify — test passes? No other tests broken?
If 3+ Fixes Failed: See Two-Attempt Rule above. STOP and discuss fundamentals.
Advanced Techniques
Git Bisect
git bisect start
git bisect bad # Current is bad
git bisect good v1.0.0 # This was good
git bisect good # or bad, repeat until found
git bisect reset
Differential Debugging
| Aspect | Working | Broken |
|---|---|---|
| Environment | Dev | Prod |
| Runtime version | 18.16.0 | 18.15.0 |
| Data | Empty DB | 1M records |
| Config | Default pool=5 | Custom pool=20 |
Condition-Based Waiting & Test Pollution
If the failure is intermittent or involves async timing, read references/condition-based-waiting.md for wait strategies before writing polling loops or arbitrary sleeps.
If the root cause traces through multiple upstream callers and you cannot isolate it by reading call sites alone, read references/root-cause-tracing.md for a systematic tracing procedure.
Patterns by Issue Type
| Issue Type | Investigation Approach |
|---|---|
| Intermittent | Add logging with timing, look for race conditions, stress test under load |
| Performance | Profile first — common culprits: N+1 queries, unnecessary re-renders, sync I/O in async paths |
| Production-only | Gather evidence (Sentry/logs/metrics), map local vs prod differences, reproduce under equivalent conditions |
| Connection/resource exhaustion | Monitor pool metrics, check for leaks in error paths, look for N+1 patterns, check if finally/defer cleanup runs |
| Data-dependent | Identify which data triggers it, find the minimum failing dataset, check for encoding/null/edge cases |
For language-specific debugging tools (breakpoints, profilers, stack traces), see the corresponding language reference files.
Red Flags — STOP and Return to Phase 1
- "Quick fix for now, investigate later"
- "Just try changing X and see"
- "I don't fully understand but this might work"
- "One more fix attempt" (when already tried 2+)
| Excuse | Reality |
|---|---|
| "Issue is simple" | Simple issues have root causes too |
| "Emergency, no time" | Systematic is FASTER than thrashing |
| "Multiple fixes saves time" | Can't isolate what worked |
| "I see the problem" | Seeing symptoms ≠ understanding root cause |
| "Just increase the pool size" | Treating symptoms hides the leak |
Never Mask Errors
| Masking Pattern | Do Instead |
|---|---|
catch (e) { /* ignore */ } |
Handle meaningfully or propagate |
if (x != null) around internal logic |
Fix why x is null |
@Disabled / skip() on failing test |
Fix the test or file tracked issue |
| Try-catch wrapping entire function | Catch specific exceptions at boundaries |
| Defensive null checks hiding broken contracts | Fix the broken contract upstream |
If unfixable now: log it, track it, surface it. Never silence it.
Quick Debugging Checklist
- Spelling errors / typos
- Case sensitivity
- Null/undefined values
- Off-by-one errors
- Async timing / race conditions
- Scope issues / type mismatches
- Missing dependencies / env vars
- Cache / stale state
- Error path cleanup (connections, file handles, locks)
- Environment differences (local vs CI vs prod)
Converted and distributed by TomeVault — claim your Tome and manage your conversions.