Debugging
The key is to reduce the problem space: turn a weird outcome into a testable hypothesis.
When you feel stuck, it’s usually because too many variables are moving at once. Your job is to freeze variables until the bug becomes deterministic.
A repeatable workflow
- Reproduce reliably.
- Freeze inputs and isolate the smallest failing unit.
- Prove or disprove one hypothesis at a time.
This sounds slow, but it’s the fastest path to a real fix. Debugging is a search problem: constrain the search space.
Common sources of bugs
- Stale closures: reading old state/props inside async callbacks.
- Effects with wrong dependencies: missing deps, or doing too much inside one Effect.
- Race conditions: multiple requests updating the same state out of order.
- Assuming “setState is immediate”: state is a snapshot for the current render.
React Strict Mode note
In development, React may run effects twice (`mount` → `cleanup` → `mount`) to help you find missing cleanups. If a bug only happens in dev, check your cleanup and idempotency.
A reliable heuristic: if your effect starts something (timer, subscription, request), it must stop it in cleanup.
Trace state and effects
type Event =
| { type: 'input'; value: string }
| { type: 'request-start' }
| { type: 'request-done'; ok: boolean };
export function trace(e: Event) {
const time = new Date().toISOString();
console.log(time, e.type, e);
}Make logs actionable
Prefer structured logs over scattered `console.log`. Include correlation ids and durations so you can reconstruct the timeline.
type TraceMeta = { scope: string; id?: string };
export function createTracer(meta: TraceMeta) {
return {
info(event: string, data?: Record<string, unknown>) {
console.log(new Date().toISOString(), meta.scope, meta.id ?? '-', event, data ?? {});
},
time<T>(event: string, run: () => T) {
const start = performance.now();
const result = run();
const durationMs = Math.round(performance.now() - start);
console.log(new Date().toISOString(), meta.scope, meta.id ?? '-', event, { durationMs });
return result;
},
};
}Isolate and bisect
When you can’t reason about the full system, stop trying. Replace unknowns with constants and remove half the code until the behavior becomes explainable.
- Remove half of the component tree or logic branches to see if the bug remains.
- Freeze inputs: hardcode props/state/network responses to make behavior deterministic.
- Turn timing bugs into data bugs: add `abort`, request ids, and ignore stale results.
Production checklist
- You can reproduce the bug with a minimal scenario and steps.
- Inputs and outputs are logged with correlation ids.
- Effects are idempotent and have correct cleanup.
- Async flows handle race conditions (abort/ignore stale).
Further reading
- Guides: Data Fetching
- Guides: Routing