A19 · Part 3 · Advanced
Debugging in production
The bug is on someone else's device, on a network you cannot reproduce.
08On this page
Why local debugging stops working
Your machine is fast, your connection is good, your cache is warm, and you have one tab open with the state you just created. None of that is true for the person filing the bug. The skill at this level is getting information back from a session you were not in.
Errors: make them arrive with context
<ErrorBoundary
onError={(error, info) => {
reportError(error, {
componentStack: info.componentStack, // which component threw
route: location.pathname,
release: __APP_VERSION__, // which deploy
userId: session?.id,
});
}}
/>
A stack trace from a minified bundle is unreadable, so upload source maps
from CI and never ship them to the browser. Without them you are reading
a.b is not a function at line 1, column 48000.
The componentStack is what makes a React error tractable —
the JavaScript stack tells you which function threw, the component stack tells
you where in the tree it was. React 19 also exposes
captureOwnerStack in development for the component that created
the element.
Group by release, not by message
The single most useful thing an error tracker gives you is "this started at 14:20 on Tuesday, which is when release 4.12 went out". Tag every report with a release identifier and a deploy timestamp. Most production bugs are answered by the diff, not by the stack.
Memory leaks
React leaks look the same every time: something outlived the component that created it, and it closed over a large object.
| Leak | Cause |
|---|---|
Listener on window | Effect with no cleanup |
| Interval or timeout | Same |
| Subscription to a store or socket | Same |
| A cache that only grows | A module-level Map keyed by something unbounded |
| Detached DOM nodes | A ref to a node held after unmount |
Finding one: open the Memory panel, take a heap snapshot, navigate away and back five times, take another, and compare. If retained size climbs per navigation, sort by detached nodes and look at what is holding them. The retainer path names the closure, and the closure names the effect.
Strict Mode's double mount is a leak detector: anything that leaks per mount leaks twice as fast in development, which is where you should be finding it.
Field data, not lab data
| Lab (Lighthouse, your machine) | Field (real users) |
|---|---|
| One device, one network, cold cache | Every device, every network, mixed caches |
| Good for comparing before and after | Good for knowing what is actually happening |
| Reproducible | The truth |
import { onLCP, onINP, onCLS } from "web-vitals";
onINP((metric) => send({ name: metric.name, value: metric.value, route, device }));
Look at the 75th percentile, not the mean. An average hides the quarter of users on a mid-range Android phone, and they are the ones abandoning. A metric that is fine on average and terrible at p75 is the most common performance situation there is.
Reproducing what you cannot see
- Throttle honestly. 4× CPU slowdown and Fast 3G is closer to a real user than your laptop on wifi.
- Session replay for interaction bugs — and treat it as sensitive data: mask inputs, and know what your privacy policy says.
- Breadcrumbs beat replays for most bugs. The last twenty actions before the error usually contain the cause.
- Feature flags so you can turn the suspect off without a deploy, which converts an incident into an investigation.
Hydration mismatches, which only happen in production
Warning: Text content did not match. Server: "12:04" Client: "12:05"
Locally the server and client run on one machine, one clock, one locale, one
timezone. In production they do not. Dates, relative times, locale formatting,
random ids, and anything read from localStorage are the usual
causes — and the symptom is a subtree silently re-rendering on the
client, undoing the benefit of server rendering exactly where it happened.
The order to work in
- When did it start? Check the release timeline before reading any code.
- Who is affected? One browser, one country, one device class narrows it enormously.
- Can you turn it off? Flag or roll back first, investigate second.
- Then reproduce, with the throttling and the data that the reports point at.
Interview answer, one sentence
"Production debugging is about getting information back: source maps and component stacks so the error is readable, releases tagged so you can ask when it started, and field vitals at p75 rather than lab numbers. React's own recurring bug is the leak — something that outlived its component because an effect had no cleanup — and a heap snapshot before and after five navigations finds it."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series