Pocock Skills
Debugging

Diagnose

A six-phase systematic approach to debugging any bug. Never guess. Build a loop, reproduce, hypothesise, instrument, fix, post-mortem.

The Diagnostic Framework

Debugging fails when engineers skip to instrumentation before they understand the bug. This skill enforces a strict phase order that prevents wasted instrumentation on the wrong hypothesis and prevents fixing symptoms instead of causes.

The most common debugging mistake

Jumping to Phase 4 (Instrument) before completing Phase 2 (Reproduce). You cannot instrument something you cannot reliably trigger.

Six-Phase Diagnostic Loop

1
Build Feedback Loop
2
Reproduce
3
Hypothesise
4
Instrument
5
Fix + Regress
6
Cleanup + Post-mortem

Phase 1: Build a Feedback Loop

Before anything else, build a mechanism that lets you observe the system's behavior rapidly. Without a feedback loop, every instrumentation change requires a slow manual test cycle.

1The 10 feedback loop methods

Ordered by build time (fastest first)

MethodBest forBuild time
Unit test (new or existing)Pure functions, isolated logicMinutes
REPL / console evaluationLanguage-level logic, one-off checksSeconds
Hardcoded input in main()Functions that need file/network inputMinutes
Minimal reproduction scriptComplex initialization sequences15-30 min
Integration test (new)Database, API, or service interactions30-60 min
E2E test replayUI flows, form submissions30-60 min
Log tailing + manual triggerInfrequent or hard-to-trigger bugsMinutes to set up
Feature flag isolationBugs in specific code pathsVaries
Shadow traffic / replayProduction-only bugs with real data shapeHours
Chaos injectionDistributed system failure modesHours
Iterate on the loop before diagnosing

The loop itself may be broken. Verify it by triggering a known-good state first, then a known-bad state, and confirming the loop distinguishes them. A loop that passes on broken code is worse than no loop.

// Before diagnosing, verify your loop is honest:
// 1. Introduce a deliberate bug you know about
// 2. Confirm the loop catches it
// 3. Revert the deliberate bug
// 4. Confirm the loop is now green (or still red if the original bug exists)
// Only then trust the loop as a diagnostic instrument.
Non-deterministic bugs

Some bugs do not reproduce reliably. Strategies for non-deterministic bugs:

  • Increase the loop frequency. Run the test 1000 times in a loop. A bug that occurs 1 in 100 runs will surface.
  • Add explicit timing control. Insert sleep() at key points to expose race conditions.
  • Eliminate environmental non-determinism first. Seed random number generators, mock time, fix network calls.
  • Record and replay. Capture the exact inputs that trigger the bug in production, replay them in test.
  • Log correlation IDs. If the bug only occurs under concurrent load, correlation IDs tell you which requests interleaved.
When you cannot build a feedback loop

If you genuinely cannot build a loop (production-only data, unreproducible environment), switch to the observability approach:

  1. 1Maximise logging granularity at the suspected location before the next occurrence.
  2. 2Define exactly what log output would confirm each hypothesis.
  3. 3Wait for the next occurrence and read the logs against each hypothesis.
  4. 4Do NOT instrument-and-guess. Write down your hypotheses before looking at the next occurrence.
If a bug has not been reproduced after three occurrences with maximum logging, escalate. The bug may require a production debugger, distributed tracing, or a database snapshot.

Phase 2: Reproduce

Reproduction means: you can trigger the exact failure, on demand, reliably, in a controlled environment. If you cannot do this, you are guessing.

2Confirm you have the right bug

Before spending time reproducing, confirm the bug report describes the actual failure. Many bug reports describe a symptom, not the bug.

  • What is the observed behavior? What data or state is wrong?
  • What is the expected behavior? Is this documented somewhere?
  • When did it start? Can you find the first commit or deploy where it appeared?
  • Who observed it? Is this one user's data or all users?
  • Is the reporter certain the behavior was correct before? Or is this a newly discovered long-standing issue?
Make it reproducible

A reproduction is only valid when:

  • It triggers the failure consistently (not just once).
  • It runs in an environment you control (not 'works on my machine').
  • It does not require live production data (unless the bug is data-specific).
  • It completes fast enough to iterate on (under 30 seconds ideally).
Capture the symptom precisely

Write down the exact symptom before hypothesising. Be specific about the observed value, not just that it was “wrong.”

// Too vague:
"The price is wrong."

// Precise symptom capture:
"For order #1234 with 3 items at $10.00 each, the subtotal shows
$29.97 instead of $30.00. The per-item price in the database is
stored as 1000 (cents). The display layer shows $9.99 per item.
This is a 0.01 discrepancy per item, consistent across all orders
with integer-cent prices."

// This precision will immediately suggest specific hypotheses
// (floating point rounding, off-by-one in cents conversion, etc.)

Phase 3: Hypothesise

Hypotheses are explicit, ranked, and written down before any instrumentation. This prevents confirmation bias and ensures you test the most likely cause first.

3Generate 3-5 ranked hypotheses

Each hypothesis must be:

  • Specific: 'The integer-to-decimal conversion in formatPrice() loses the fractional cent.' Not 'something in the formatting is wrong.'
  • Falsifiable: you can describe exactly what evidence would disprove it.
  • Ranked by probability: based on the symptom, which cause is most likely?
Hypotheses for the price display bug (ranked):

1. (Most likely) formatPrice() divides by 100 using integer division,
   truncating instead of rounding. Evidence that would confirm:
   formatPrice(999) returns "$9.99" but formatPrice(1000) returns
   "$9.00" instead of "$10.00".

2. The stored price is wrong. The database has 999 instead of 1000.
   Evidence: SELECT price FROM items WHERE id = [item_id] returns 999.

3. A rounding rule is applied twice: once in the ORM and once in
   the display layer. Evidence: the raw API response shows the
   correct value but the rendered value is different.

4. The currency library has a known bug with this denomination.
   Evidence: GitHub issues on the library, or a unit test of the
   library function shows the discrepancy.

5. (Least likely) A locale setting is changing decimal separators.
   Evidence: the bug only appears in non-US locales.
Show hypotheses to the user before testing

Before writing any instrumentation code, share your ranked hypotheses. This step costs under a minute and can save hours:

  • The user may know immediately which hypothesis is correct (or impossible).
  • The user may have context that rules out three of five hypotheses.
  • Presenting hypotheses builds trust and keeps the user informed of your diagnostic method.
  • It creates a record of your reasoning that others can follow.
If the user immediately says “it's definitely not hypothesis 2, I already checked the database,” you have saved yourself a trip through Phase 4 for that hypothesis.

Phase 4: Instrument

Instrumentation means adding observability to test one hypothesis at a time. The discipline is: one variable at a time, tagged logs, and perf isolation when relevant.

4One variable at a time

Never test two hypotheses simultaneously. If you add three log statements and change a function, you cannot know which change produced the result.

  • Test hypothesis 1. Get a result. Record whether it confirmed or denied the hypothesis.
  • If confirmed: move to Phase 5 (Fix). If denied: test hypothesis 2.
  • Do not modify behavior while instrumenting. Read-only observation first.
Tagged logs [DEBUG-xxxx]

All debug logging added during diagnosis must be tagged with a consistent prefix and a ticket/issue number. This makes them easy to find and remove after the fix.

// Tagged debug log format:
console.log("[DEBUG-1234] formatPrice input:", cents);
console.log("[DEBUG-1234] formatPrice output:", result);
console.log("[DEBUG-1234] division step:", cents / 100);

// After the bug is fixed, grep for [DEBUG-1234] and remove all
// instrumentation before committing.

// grep -r "DEBUG-1234" src/ --include="*.ts"
// Ensures no debug logs slip through to production.
Debug logs with personal notes, TODO comments, or no tag are the most common source of accidental production log pollution. The tag makes cleanup mechanical.
Performance branch for perf bugs

If the bug is a performance regression, instrument in a dedicated branch with performance profiling enabled. Profiling tools often change timing characteristics enough to mask the bug in production conditions.

  • Establish a baseline measurement before adding instrumentation.
  • Measure one change at a time.
  • Use wall-clock time for user-facing operations; use CPU time for compute-bound operations.
  • Do not optimize until you have confirmed the bottleneck location.

Phase 5: Fix + Regression Test

The fix addresses the root cause identified in Phase 4. The regression test ensures the bug cannot silently return.

5Write the test before the fix

The test must fail before you apply the fix and pass after. This confirms the fix actually addresses the tested behavior, not that you wrote a test that happens to pass.

// Test written BEFORE the fix — must fail:
test("formatPrice converts integer cents to decimal dollars", () => {
  expect(formatPrice(1000)).toBe("$10.00");  // FAILS before fix
  expect(formatPrice(999)).toBe("$9.99");
  expect(formatPrice(1)).toBe("$0.01");
  expect(formatPrice(0)).toBe("$0.00");
});

// Apply fix:
function formatPrice(cents: number): string {
  return "$" + (cents / 100).toFixed(2);  // was: Math.floor(cents / 100)
}

// Test now passes.
The correct seam concept

A seam is a place in the code where behavior can be changed without modifying the code that uses it. Fixing at the correct seam means:

  • Fix in the layer that owns the behavior, not the layer that first noticed the problem.
  • If display code is showing wrong values because the API returns wrong values, fix the API — not the display code.
  • If a caller misuses a function, consider whether the function should validate or defend against the misuse rather than documenting the correct calling convention.
  • Ask: 'If I fix it here, and the same bug appears elsewhere, would someone know to fix it in the same way?' If not, the seam is wrong.

Seam selection rule

The correct seam is the highest seam where the fix is complete. A fix that must be repeated across multiple call sites belongs at a lower, shared seam.

Phase 6: Cleanup + Post-Mortem

After the fix is verified, remove all instrumentation and capture the learning.

6Remove debug logs

Before committing, grep for all tagged debug logs and remove them. The tag from Phase 4 makes this a one-command operation.

# Remove all DEBUG-1234 logs before commit
grep -r "DEBUG-1234" src/ --include="*.ts" --include="*.js"
# Review each match, delete the log statement
# Run tests to confirm removal did not break the fix
npm test
State the hypothesis in the commit message

The commit message should state the root cause and the fix, not just describe the symptom.

// Bad commit message:
"Fix price display bug"

// Good commit message:
"fix: integer division truncation in formatPrice()

formatPrice() was using Math.floor(cents / 100) which truncated
fractional cents. Fixed to use (cents / 100).toFixed(2) to preserve
decimal precision.

Regression test added: formatPrice() now tested with values that
produce non-zero decimal places (1, 99, 1001).
Ask what would have prevented this

After every non-trivial bug fix, ask three questions:

  • What test would have caught this at the time it was introduced?
  • What type system change would have made this class of bug impossible?
  • What monitoring or alerting would have surfaced this before a user reported it?

Each question should produce a concrete artifact: a new test, a type definition change, or an alert configuration. If the answer to all three is “nothing,” the post-mortem is incomplete.

HITL (Human In The Loop) Template

When debugging requires human action (e.g., running a command on a production server, providing access to a database, triggering a manual workflow), use this template to communicate clearly and minimise the number of round-trips.

HITL loop template
## HITL Request — Diagnosis Phase [N]

**What I need from you:**
[Single, specific action. One request per HITL.]

**Why I need it:**
[Which hypothesis does this test? What will the result tell me?]

**What to do:**
[Exact command or steps, copy-pasteable]

**What to send back:**
[Exact output format — "paste the full output of X" or "screenshot
of Y" or "the value shown in field Z"]

**How long this should take:**
[Estimate. If it takes longer, something is wrong with the
instructions.]

**What I will do with the result:**
[This tells you my next step before you run anything, so you can
flag if my plan is wrong.]

---

Example:

**What I need from you:**
Run a SELECT against the production orders table for order #1234.

**Why I need it:**
Testing hypothesis 2: the stored price is 999 instead of 1000.

**What to do:**
SELECT id, price_cents, created_at FROM order_items WHERE order_id = 1234;

**What to send back:**
Paste the full query result.

**How long this should take:**
Under 30 seconds.

**What I will do with the result:**
If price_cents = 999, hypothesis 2 is confirmed and I will check
the write path. If price_cents = 1000, hypothesis 2 is denied and
I will move to hypothesis 1 (the formatPrice() function).