systematic-debugging
Use when encountering any bug, test failure, or unexpected behavior — find root cause before proposing any fix
Systematic Debugging
Overview
Random fixes waste time and mask underlying issues. Core principle: Find root cause before attempting any fix. Symptom fixes are failure.
The Iron Law
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
When to Use
Use for ANY technical issue. Especially when under time pressure, when you've already tried multiple fixes, or when you don't fully understand the issue.
The Four Phases
Complete each phase before proceeding.
Phase 1: Root Cause Investigation
- Read Error Messages — note line numbers, file paths, stack traces
- Reproduce Consistently — identify exact steps to trigger the bug reliably
- Check Recent Changes — review git diffs, new dependencies, config changes
- Gather Evidence — in multi-component systems, log at boundaries to isolate the failing layer
- Trace Data Flow — for deep errors, trace backward through the call chain to the original trigger
Phase 2: Pattern Analysis
- Find Working Examples — locate similar working code in the codebase
- Compare Against References — read reference implementations completely
- Identify Differences — list every difference between working and broken states
Phase 3: Hypothesis and Testing
- Form Single Hypothesis — "I think X is the root cause because Y"
- Test Minimally — make the SMALLEST change to test the hypothesis
- Verify — if it fails, form a NEW hypothesis; never stack fixes
Phase 4: Implementation
- Create Failing Test — simplest reproduction (use
test-driven-development) - Implement Single Fix — address the identified root cause only
- Verify Fix — test passes and no regressions
- If 3+ Fixes Fail — STOP; question the architecture; discuss with your human partner
Red Flags — Return to Phase 1
- "Quick fix for now"
- Adding multiple changes at once
- Proposing solutions before tracing data flow
- Each fix reveals a new problem elsewhere
- Skipping test creation
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Issue is simple" | Simple bugs have root causes too |
| "Emergency" | Systematic is FASTER than thrashing |
| "Test after fix" | Untested fixes don't stick |
| "3+ failures" | Architectural problem — question the pattern |
Quick Reference
| Phase | Key Activity | Success Criteria |
|---|---|---|
| 1. Root Cause | Read errors, reproduce, trace data flow | Understand WHAT and WHY |
| 2. Pattern | Find working examples, compare | Identified differences |
| 3. Hypothesis | Form theory, test minimally | Confirmed hypothesis |
| 4. Implementation | Create test, fix, verify | Bug resolved, tests pass |
Supporting Techniques
Root Cause Tracing
Trace backward through the call chain until you find the original trigger, then fix at the source — not where the symptom appears.
- Observe the symptom (error message, wrong output)
- Find the immediate cause (what code directly produces this?)
- Ask: what called this, and with what values?
- Keep tracing up the call stack
- Find where the bad value originates — fix there
When you can't trace manually, add instrumentation at the suspect boundary:
console.error('DEBUG:', { inputValue, cwd: process.cwd(), stack: new Error().stack });
Use console.error in tests (not logger — may not show).
Defense in Depth
Validate at every layer data passes through. Make the bug structurally impossible.
| Layer | Purpose |
|---|---|
| Entry point | Reject invalid input at API boundary |
| Business logic | Ensure data makes sense for this operation |
| Environment guards | Prevent dangerous operations in specific contexts (e.g., refuse destructive ops outside temp dir during tests) |
| Debug instrumentation | Capture context for forensics |
Condition-Based Waiting
Wait for the actual condition, not a guess about timing.
| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for file | waitFor(() => fs.existsSync(path)) |
Generic polling function: poll every 10ms, timeout at 5000ms, throw with descriptive message on timeout.
Related Skills
- test-driven-development — for creating the failing test in Phase 4
- completing-work — verify fix worked before claiming success