React Error Boundaries — The $120k Blank Dashboard
- Error boundaries catch render-time errors only — use them to isolate crashing components.
- Place boundaries strategically: global for safety, granular for resilience.
- Always log errors via componentDidCatch to your monitoring service.
- 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
React Error Boundary Debug Cheat Sheet
Blank screen, no error boundary fallback
Look for uncaught errors in console — if none, React swallowed it silentlywindow.__REACT_DEVTOOLS_GLOBAL_HOOK__: check if React is mountedError boundary fires but fallback is blank
Render the fallback component directly outside the boundary to testAdd console.log inside componentDidCatch to verify error infoOnly some users see the fallback
Enable full source maps in production buildUse error logging service to group errors by browser and component stackProduction Incident
undefined when the API returned an empty data array. React's default behaviour unmounted the entire component tree above the failing component.Production Debug GuideSymptom → Immediate Action
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:
import React from 'react'; export class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, info) { console.error('Error caught by boundary:', error, info); // Send to monitoring service like Sentry logErrorToService(error, info.componentStack); } render() { if (this.state.hasError) { return <div>Something went wrong. Please try again.</div>; } return this.props.children; } }
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.
function ClickHandler() { const handleClick = async () => { try { await fetchData(); } catch (error) { // Error boundaries can't catch this, handle it here showToast('Fetch failed'); } }; return <button onClick={handleClick}>Load Data</button>; }
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:
import { render, screen } from '@testing-library/react'; import { ErrorBoundary } from './ErrorBoundary'; const ThrowComponent = () => { throw new Error('Test crash'); }; test('error boundary catches error and shows fallback', () => { render( <ErrorBoundary> <ThrowComponent /> </ErrorBoundary> ); expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument(); });
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.
import React from 'react'; export class RetryBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } handleRetry = () => { this.setState({ hasError: false, error: null }); }; render() { if (this.state.hasError) { return ( <div role="alert"> <p>Something went wrong.</p> <button onClick={this.handleRetry}>Retry</button> </div> ); } return this.props.children; } }
| Mechanism | Scope | Can Catch Render Errors? | Can Catch Async Errors? | Production Use Case |
|---|---|---|---|---|
| Error Boundary (class component) | Child component tree | Yes | No | Wrap widgets, routes, third-party libraries |
| try-catch in event handler | That handler only | No | Yes (if inside handler) | Handle failed API calls, form submissions |
| window.onerror / global error handler | Whole page | Yes (but no component stack) | Yes | Last-resort logging, cannot show fallback UI |
| unhandledrejection event | Promise rejections | No | Yes | Catch rejected Promises outside render |
🎯 Key Takeaways
- Error boundaries catch render-time errors only — use them to isolate crashing components.
- Place boundaries strategically: global for safety, granular for resilience.
- Always log errors via componentDidCatch to your monitoring service.
- Add retry logic with a cap to give users a way to recover.
- Test your boundaries with a throwing child and ensure the fallback is self-contained.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QExplain how React error boundaries work and what they can and cannot catch.SeniorReveal
- QHow would you test an error boundary component?Mid-levelReveal
- QWhat is the difference between getDerivedStateFromError and componentDidCatch?SeniorReveal
Frequently Asked Questions
What is the simplest way to add an error boundary to an existing app?
Create a class component that implements getDerivedStateFromError and componentDidCatch. Wrap your existing component tree — start with a root-level boundary, then add granular boundaries around sections that use third-party libraries or external data.
Can I use hooks to create an error boundary?
No — error boundaries require the class component lifecycle methods getDerivedStateFromError and componentDidCatch. Hooks do not have equivalents. However, you can create a reusable error boundary component once and use it throughout your app.
How do I handle errors in event handlers that fire before the component renders?
Use try-catch inside the event handler function. Show a user-friendly error message or toast. If the error is severe, you can programmatically set state to trigger an error boundary by calling a function that throws during the next render, but that's not recommended.
Does the order of multiple error boundaries matter?
Yes — the closest boundary to the crashing component catches the error. If you have a child boundary inside a parent, the child catches first. This allows you to have a granular fallback for a widget and a more generic fallback for the whole page if the widget's boundary fails.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.