Systematic Debugging
When to Use
- A bug, error, exception, or crash needs investigation
- Something is "not working" and the cause is unclear
- A test is failing and the reason isn't obvious
- Unexpected behavior needs troubleshooting in any language or framework
Core Workflow
Follow these five phases sequentially. Do not skip ahead to fixing before completing isolation and tracing.
Phase 1: Reproduce
Establish a reliable way to trigger the bug before doing anything else.
- Read the full error message, stack trace, and logs — note exact text, line numbers, and error codes
- Create a minimal reproduction case that triggers the issue consistently
- Record the exact steps, inputs, and environment that cause the failure
Checkpoint: Can you trigger the bug on demand? If intermittent, gather more data before proceeding.
Phase 2: Isolate
Narrow down where the failure originates.
- Use binary search to find the failing component — disable or stub out halves of the system
- Check recent changes with
git log --oneline -20andgit diffagainst the last known good state - Add targeted logging or use a debugger to observe state at key boundaries
# Find which commit introduced the bug
git bisect start
git bisect bad HEAD
git bisect good <last-known-good-commit>
# Git will checkout midpoints — test each one and mark good/bad
Checkpoint: The bug is traced to a specific function, module, or data flow.
Phase 3: Trace to Root Cause
Understand why the failure happens — not just where.
- Read the code path completely from entry point through the failure site
- Check assumptions: what does each function expect vs. what it actually receives?
- Trace data flow backward — where does the bad value originate?
- Verify with evidence: add assertions or print statements to confirm your hypothesis
# Example: verify assumptions about incoming data
def process_order(order):
assert order.status == "pending", f"Expected pending, got {order.status}"
assert order.items, "Order has no items"
# ... rest of processing
Checkpoint: The chain of causation from trigger to symptom is explained, with supporting evidence (logs, assertions, debugger output).
Phase 4: Fix at Root Cause
Apply a targeted fix that addresses the actual cause, not just the symptom.
- Fix the root cause, not a downstream effect
- Keep the fix minimal — change only what's necessary
- Avoid "band-aid" fixes that mask the underlying problem (e.g., adding a try/except around a crash without fixing why it crashes)
Phase 5: Verify
Confirm the fix works and doesn't introduce regressions.
- Run the reproduction case from Phase 1 — confirm the bug is gone
- Run the full test suite to check for regressions
- Test edge cases related to the fix
- If the bug was missing a test, add one that would have caught it
Checkpoint: Reproduction case passes, test suite is green, and you have a new test covering this bug.
Key Anti-Patterns to Avoid
- Shotgun debugging: making random changes hoping something works
- Fix-and-pray: applying a fix without understanding the cause
- Skipping reproduction: jumping to code changes without confirming you can trigger the bug
- Fixing symptoms: wrapping errors in try/catch instead of fixing what produces them
See anti-patterns.md for the full catalog.
Related Skills
- root-cause-tracing: Deep call-stack tracing techniques — use after Phase 2 when the bug is deep in execution chains
- verification-before-completion: Mandatory verification gates — reinforces Phase 5 before claiming a fix is complete
Deep-Dive References
- workflow.md: Detailed phase-by-phase instructions with decision trees
- examples.md: Worked debugging examples across languages
- troubleshooting.md: Common debugging scenarios and solutions
- anti-patterns.md: Patterns to avoid and how to recognize them