| name | debugging-workflow |
| description | Systematic debugging methodology — reproduction, isolation, binary search, profiling, distributed tracing, memory leaks, and race conditions. A structured approach that prevents random guessing and finds root causes faster. |
Debugging Workflow
Bugs are found by narrowing the search space, not by guessing. Every technique here reduces possibilities.
When to Activate
- A bug is not immediately obvious after reading the code
- An intermittent failure (flaky test, race condition)
- A performance regression (something got slower)
- A memory leak or out-of-memory crash
- A "works on my machine" environment mismatch
- Production incident requiring root cause analysis
Universal Debugging Process
1. REPRODUCE Make the bug happen reliably
2. ISOLATE Shrink the failing case to minimum
3. HYPOTHESIZE Form ONE specific, falsifiable hypothesis
4. VERIFY Test the hypothesis (binary search if needed)
5. FIX Change only what's needed
6. CONFIRM Verify fix + run regression tests
Never skip steps. Jumping to "fix" before "reproduce" reliably is how you get false fixes.
Step 1 — Reproduce Reliably
Before anything else, make the bug deterministic.
node --version && npm --version
cat package-lock.json | grep '"lockfileVersion"'
for i in {1..50}; do npx vitest run --reporter=verbose 2>&1 | grep -E "FAIL|PASS"; done
npx vitest run --seed 12345
go test -race -count=100 ./...
If you can't reproduce, you can't debug. Fix the environment difference first.
Step 2 — Isolate (Minimal Reproduction)
Remove everything that isn't part of the bug.
git bisect start
git bisect bad HEAD
git bisect good v1.2.0
git bisect good
git bisect reset
For code bugs, reduce the test case:
- Remove unrelated modules one by one
- Comment out code until bug disappears
- The last removed piece is in the causal chain
- Re-add just that piece → minimal reproduction
Step 3 — Instrument Before Guessing
Add targeted logging, not random print statements everywhere.
Node.js / TypeScript
const DEBUG = process.env.DEBUG?.includes('orders');
function createOrder(input: CreateOrderInput) {
DEBUG && console.log('[createOrder] input:', JSON.stringify(input));
const validated = validate(input);
DEBUG && console.log('[createOrder] validated:', validated);
const result = db.insert(validated);
DEBUG && console.log('[createOrder] result:', result);
return result;
}
async function timed<T>(label: string, fn: () => Promise<T>): Promise<T> {
const start = performance.now();
try {
const result = await fn();
console.log(`[timing] ${label}: ${(performance.now() - start).toFixed(2)}ms`);
return result;
} catch (err) {
console.log(`[timing] ${label}: FAILED after ${(performance.now() - start).toFixed(2)}ms`);
throw err;
}
}
const result = await timed('db.findOrder', () => db.orders.findById(id));
Go
log.Printf("[createOrder] userId=%s items=%d", req.UserID, len(req.Items))
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
go test -race ./...
go run -race main.go
Python
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s %(message)s')
logger = logging.getLogger(__name__)
import pdb; pdb.set_trace()
python -m cProfile -o profile.out script.py
python -m pstats profile.out
Performance Debugging
Slow Endpoint — Systematic Approach
curl -w "\nTotal: %{time_total}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\n" \
-o /dev/null -s https://api.example.com/orders/123
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
res.setHeader('X-Response-Time', `${Date.now() - start}ms`);
});
next();
});
Database Query Analysis
SELECT query, mean_exec_time, calls, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = '123' ORDER BY created_at DESC LIMIT 10;
CREATE INDEX CONCURRENTLY idx_orders_user_id_created ON orders(user_id, created_at DESC);
EXPLAIN ANALYZE ...
N+1 Query Detection
const db = drizzle(pool, {
logger: {
logQuery(query, params) {
console.log('[SQL]', query, params);
}
}
});
Memory Leak Debugging
Node.js
node --expose-gc server.js
global.gc();
const { heapUsed } = process.memoryUsage();
console.log(`Heap: ${(heapUsed / 1024 / 1024).toFixed(2)} MB`);
Common Node.js memory leak sources:
emitter.on('data', handler);
const cache = new Map();
cache.set(key, value);
function createHandler(largeData: Buffer) {
return () => console.log(largeData.length);
}
Go
import "net/http/pprof"
import _ "net/http/pprof"
go tool pprof http:
# top10
# list <function-name>
# web (flame graph, requires graphviz)
Race Condition Debugging
go test -race ./...
var cache = map[string]string{}
go func() { cache[key] = value }()
for _, item := range items {
go func() {
process(item)
}()
}
let result;
fetchA().then(a => { result = a; });
fetchB().then(b => { result = b; });
useEffect(() => {
let cancelled = false;
fetchOrder(id).then(order => {
if (!cancelled) setOrder(order);
});
return () => { cancelled = true; };
}, [id]);
Distributed Tracing (Production)
Cross-reference: The OpenTelemetry setup and instrumentation patterns below are also covered in depth in the observability skill, including collector configuration, sampling strategies, and Grafana/Jaeger dashboard setup. Use this section for debugging context; use observability for production rollout.
When a request is slow and you don't know which service caused it:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({ url: 'http://jaeger:4318/v1/traces' }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function processOrder(orderId: string) {
return tracer.startActiveSpan('processOrder', async (span) => {
span.setAttribute('order.id', orderId);
try {
const result = await doWork(orderId);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
}
Reading traces in Jaeger/Grafana Tempo:
- Find the slow root trace
- Expand child spans — find the longest one
- Drill into that service's spans
- Repeat until you find the leaf span (usually a DB query or external call)
Debugging Checklist
Reproduce
□ Bug happens deterministically
□ Minimal reproduction case created
Gather Evidence
□ Error message + full stack trace collected
□ Input that triggers the bug identified
□ Environment diff checked (versions, env vars, data)
□ git bisect run if regression
Instrument
□ Targeted logging added at suspicious points
□ Query explain plan checked (if DB involved)
□ Profiler/tracer attached (if performance issue)
Hypothesis
□ ONE specific hypothesis formed
□ Prediction: "if X is the cause, then Y will happen when I do Z"
Verify
□ Test confirmed or refuted hypothesis
□ Root cause identified (not just symptom)
Fix
□ Minimal change applied
□ All tests pass
□ Regression test added for this bug
□ Debug logging removed