React Error Boundaries — The $120k Blank Dashboard
A single uncaught render error unmounts entire React tree — blank dashboard cost one team $120k.
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
- Error boundaries catch render-time JavaScript errors in the React component tree
- They work via getDerivedStateFromError (render fallback) and componentDidCatch (side effects)
- Only class components can be error boundaries — hooks don't have the lifecycle methods
- A single boundary can protect an entire subtree, but granular boundaries reduce user impact
- Production gotcha: error boundaries catch render errors, not event handler or async code errors
- Performance trade-off: wrapping every widget increases bundle size and maintenance overhead
Picture a fuse box in your house. If one appliance short-circuits, only that room's fuse blows — the rest of your house stays lit. Without it, the whole house goes dark. A React Error Boundary is that fuse: it contains a crash inside one part of your UI so the rest of the page keeps working perfectly. Without it, a single broken component throws your entire React tree into a blank screen.
Production React apps break. A payment widget fetches malformed JSON, a third-party chart library throws on null data, a lazy-loaded component fails to parse — and suddenly your user sees a completely blank page with no explanation. No error message, no fallback, just white. That is the default behaviour of an unhandled render error in React, and it is brutal. A blank screen is worse than a visible error because users assume the whole product is broken, not just one widget.
What Is React Error Boundaries?
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the crashed component tree. They work like a try-catch block for the render phase — but only for class components. The two key methods are getDerivedStateFromError (to update state and render a fallback) and componentDidCatch (to log side effects). Here's a minimal example:
How Error Boundaries Work Internally
When a component throws during rendering, React walks up the component tree looking for the nearest parent error boundary. If it finds one, React calls getDerivedStateFromError with the thrown error, then re-renders the boundary with the updated state (hasError = true). The fallback UI is rendered instead of the crashed subtree. If no boundary is found, React unmounts the entire tree and logs the error to the console. This 'catch-and-render' mechanism only applies to errors thrown in the commit phase — not during reconciliation itself. React uses a special work loop that catches thrown values and checks for boundary components that implement either componentDidCatch or getDerivedStateFromError.
- Each net catches failures from the floors below it.
- If the net is at the ground floor (root level), any fall from any floor brings down the whole building.
- Nets on every floor (granular) limit damage to a single room.
- The net itself must be sturdy — it can't be made of the same flimsy material as the floors it protects.
Where to Place Error Boundaries in Your Component Tree
The placement of error boundaries is a strategic decision. You have two extremes: a single boundary at the root (global), which shows a full-page error for any crash, or granular boundaries around each widget. A common production pattern is a layered approach: - One boundary around the entire app to catch truly catastrophic failures. - Separate boundaries around each major section (header, sidebar, main content). - Additional boundaries around third-party widgets, charts, or data-heavy components. This lets the rest of the page remain interactive even if a widget fails. The trade-off is more code and potential memory overhead if you create too many boundary instances.
What Error Boundaries Cannot Catch
This is the most common source of confusion. Error boundaries do NOT catch: - Errors inside event handlers (e.g., onClick, onChange) — use try-catch inside the handler. - Errors in asynchronous code (setTimeout, Promises, async/await) — wrap the async block or use a global promise rejection handler. - Errors thrown during server-side rendering (SSR) — boundaries only work in the client. - Errors in the boundary component itself — the boundary cannot catch its own errors. - Errors thrown during event bubbling or in context providers — those are outside the render phase. For these cases, you need a separate error handling strategy: try-catch in event handlers, window.onerror for unhandled client exceptions, and error boundaries only for rendering.
Testing Error Boundaries
You should test that your error boundary correctly renders the fallback when a child throws. Use Jest with React Testing Library. The key is to simulate a rendering error in a child component. Then assert that the fallback UI appears. Also test that the componentDidCatch is called with the correct error. Here's an example:
Production Patterns with Error Boundaries
In production apps, a single error boundary is rarely enough. Common patterns include: - Retry pattern: After showing the fallback, give the user a 'Retry' button that resets the boundary state by calling setState({ hasError: false }). - Logging pattern: In componentDidCatch, send the error to your monitoring service along with the component stack. - Bulkhead pattern: Wrap each micro-frontend or module in its own boundary so that a failure in one doesn't affect others. - Reset pattern: If the error was a transient network failure, you can clear the error boundary after a timeout and re-mount the children by forcing a key change on the boundary's container.
Why Error Boundaries Can't Touch Event Handlers or Async Code
You just shipped a feature. A user clicks a button, and the whole screen goes white. Your error boundary sat there doing nothing. Why? Because error boundaries only catch errors thrown during React's reconciliation phase — render, lifecycle methods, constructors, and effects. Event handlers run outside that phase. So do setTimeout, Promise chains, and requestAnimationFrame. If an error happens in an onClick handler, React has already committed the VDOM. There's no rendering to roll back. The error propagates to the window, not your boundary. Production apps handle this with a one-liner: wrap your async operations in try-catch and pipe errors into state (or a global error handler). Logging is cheap. White screens are not.
How react-error-boundary Saves You from Class Component Boilerplate
Maintaining a class component just for error handling feels like stepping back to 2018. The react-error-boundary library gives you a hook-based fallback that actually gets used in production. You wrap the boundary component around a tree. Pass a FallbackComponent or fallback prop. That's it. Under the hood, it still uses componentDidCatch — that's non-negotiable — but the API is declarative. You get onError for logging, onReset for retry logic, and resetKeys to clear state on prop changes. No more writing constructor+toggle state+render-if pattern in fifteen files. One import. One wrap. Keep your codebase modern. This is what your team should standardize on today.
The Two Real Limitations Nobody Talks About
Every blog post tells you boundaries don't catch async errors. Fine. But here's what hits you in production: boundaries cannot catch errors thrown inside the boundary component itself. If your ErrorBoundary's render fails, React has no fallback — it re-throws the error up the tree. Second: boundaries cannot catch errors in server-side rendered output. React throws a hard error during SSR, no boundary intervenes. This means your next.js or remix app during SSR hydration mismatch will kill the node process unless you have a global process.on('uncaughtException'). The fix? Keep your boundary component dead simple — no third-party deps, no state management, just a literal display. For SSR, implement a custom error page route. Don't let the docs fool you: boundaries are client-side only.
The Blank Dashboard That Cost $120k
undefined when the API returned an empty data array. React's default behaviour unmounted the entire component tree above the failing component.- Every third-party component that renders user-controlled data needs its own error boundary.
- A single uncaught render error unmounts the whole tree — your fallback UI must be deliberate.
- Test with empty, null, and malformed data in component tests.
Look for uncaught errors in console — if none, React swallowed it silentlywindow.__REACT_DEVTOOLS_GLOBAL_HOOK__: check if React is mountedKey takeaways
Common mistakes to avoid
4 patternsPlacing a single error boundary at the root level
Not logging errors in componentDidCatch
Forgetting that error boundaries don't catch async errors
Using error boundaries to catch errors in event handlers
Interview Questions on This Topic
Explain how React error boundaries work and what they can and cannot catch.
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
That's React.js. Mark it forged?
4 min read · try the examples if you haven't