Senior 4 min · March 06, 2026

React Error Boundaries — The $120k Blank Dashboard

A single uncaught render error unmounts entire React tree — blank dashboard cost one team $120k.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
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.

Picture a fuse box in your house.

The two key methods are getDerivedStateFromError (to update state and render a fallback) and componentDidCatch (to log side effects). Here's a minimal example:

Plain-English First

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:

ErrorBoundary.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
  }
}
Output
Usage: <ErrorBoundary><Widget /></ErrorBoundary>
Forge Tip:
Write the fallback UI yourself — don't rely on a generic 'Something went wrong' message. Show a retry button, link to support, or a cached version of the content.
Production Insight
Error boundaries only catch errors during render, lifecycle methods, and constructors.
They do NOT catch errors in event handlers, async code (setTimeout, Promises), or server-side rendering.
Rule: always wrap async operations in their own try-catch or redirect to a boundary reporting mechanism.
Key Takeaway
Implement error boundaries as class components with getDerivedStateFromError and componentDidCatch.
Place them strategically around failure-prone UI sections.
Remember that async code and event handlers are invisible to boundaries.
React Error Boundaries Flow THECODEFORGE.IO React Error Boundaries Flow How error boundaries catch and handle rendering errors Error Boundary Component Class component with componentDidCatch Rendering Error Uncaught exception in child tree componentDidCatch Triggered Receives error and errorInfo Fallback UI Rendered Replaces crashed subtree Error Boundary Placement Wrap risky components or routes ⚠ Cannot catch event handler errors Use try-catch inside handlers instead THECODEFORGE.IO
thecodeforge.io
React Error Boundaries Flow
React Error Boundaries

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.

Mental Model: The Safety Net
  • 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.
Production Insight
React's error boundary traversal is O(depth of tree) — not a performance problem.
What matters is that getDerivedStateFromError runs during the render phase, so it must be a pure function with no side effects.
Use componentDidCatch for logging – it runs after the commit and has access to the full component stack.
Key Takeaway
Error boundaries work by bubbling errors up the tree until a boundary is found.
getDerivedStateFromError sets the fallback state; componentDidCatch handles side effects.
No boundary = entire tree unmounts.

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.

Production Insight
Too many boundaries made it hard to trace the root cause of errors — you'd see a fallback but not know which widget broke.
We added a unique identifier to each boundary that logs along with the error info.
Rule: each boundary should have a human-readable name for easier debugging.
Key Takeaway
Don't wrap everything — wrap failure-prone components.
Use a layered approach: global + granular.
Name your boundaries to simplify log analysis.
Where to Place Your Next Error Boundary?
IfComponent renders data from an external API
UseWrap it individually — malformed responses are common
IfComponent uses a third-party UI library
UseWrap it — library updates can introduce rendering bugs
IfComponent is a static header or footer
UseNo boundary needed — they rarely throw and global boundary covers them
IfComponent is a lazy-loaded page route
UseWrap the lazy component — network failures can cause chunk load errors

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.

AsyncErrorFix.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
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>;
}
Output
Always use try-catch in event handlers and async functions.
Common Pitfall: Async Errors
Don't assume your error boundary will catch a failed fetch inside a useEffect. That error occurs in the effect callback, which is not part of the render phase. You must catch it yourself.
Production Insight
A production bug where a Promise rejection in a useEffect caused an uncaught error — but the component still rendered a partial UI.
The error boundary never fired because the rejection was outside the render phase.
We added a global unhandledrejection handler that logs to the monitoring system and shows a toast.
Key Takeaway
Error boundaries only cover render-time errors.
Use try-catch for event handlers, async functions, and effects.
Add global error handlers for the rest.

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:

ErrorBoundary.test.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
});
Output
PASS: renders fallback UI when child throws
Production Insight
We had a bug where the fallback itself threw because it tried to access a context provider that wasn't available after the crash.
We fixed it by making the fallback a simple static component with no dependencies.
Rule: never let the fallback rely on context or external state that may be corrupted.
Key Takeaway
Always test error boundaries with a throwing child.
Ensure the fallback is self-contained and doesn't depend on external state.
Also test that errors are logged to your monitoring service.

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.

RetryBoundary.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
  }
}
Output
Retry button resets boundary state, re-rendering children.
Production Insight
Retry logic can cause infinite loops if the error is persistent — always add a max retry count.
We also used a fallback that shows cached data from localStorage if available.
Rule: never retry more than 3 times without alerting the user.
Key Takeaway
Add retry functionality with a cap (max 3 retries).
Log every error to your monitoring service.
Consider showing cached data in the fallback to preserve user experience.

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.

Example.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge

function Button({ label }) {
  const [error, setError] = useState(null);

  const handleClick = async () => {
    try {
      const data = await fetchOrder();
      // process data
    } catch (err) {
      setError(err);
    }
  };

  if (error) return <ErrorFallback error={error} />;

  return <button onClick={handleClick}>{label}</button>;
}
Output
When fetch fails, component renders ErrorFallback instead of crashing the tree.
Production Trap:
Never rely on an error boundary to catch event handler errors. Always pair boundaries with explicit try-catch handlers in UI callbacks.
Key Takeaway
Error boundaries are rendering guards, not global catchalls. Wrap async and event handler errors yourself.

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.

Example.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge

import { ErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>Something broke: {error.message}</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={Fallback}
      onError={(error) => console.error('Boundary caught:', error)}
      resetKeys={['userId']}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}
Output
Dashboard errors render a fallback with retry. Resets when userId changes.
Production Insight:
Standardize on react-error-boundary across your team. One pattern for all boundaries eliminates the class component tango and makes error recovery consistent.
Key Takeaway
Stop writing class boundaries. Use react-error-boundary for hook-based, production-ready error handling with retry and logging built in.

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.

Example.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge

// BAD — boundary itself can fail
class FragileBoundary extends React.Component {
  componentDidCatch(error) {
    // imagine this fetch throws
    fetch('/log', { method: 'POST' });
  }
  render() {
    return <ExpensiveWidget />;  // if this throws, no recovery
  }
}

// GOOD — minimal boundary
class SolidBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  render() {
    if (this.state.hasError) {
      return <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}
Output
SolidBoundary renders a static fallback. No network calls, no complex children in the boundary itself.
Production Trap:
Never put data fetching or complex logic inside your boundary component. One uncaught error there and your fallback never shows — you get a blank white page.
Key Takeaway
Error boundaries are client-only, and the boundary component itself must never throw. Keep it stateless and minimal.
● Production incidentPOST-MORTEMseverity: high

The Blank Dashboard That Cost $120k

Symptom
Users saw a completely white page when opening the dashboard. No error message, no fallback. The app was rendered unresponsive.
Assumption
The team assumed that because the chart library was 'stable', it would never throw during render. They had no error boundary anywhere in the tree.
Root cause
The chart library attempted to access a property on undefined when the API returned an empty data array. React's default behaviour unmounted the entire component tree above the failing component.
Fix
Wrap each widget (including the chart) in its own error boundary. Added a fallback UI that shows 'Widget unavailable' with a retry button. Deployed a regression test that checks for empty data.
Key lesson
  • 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.
Production debug guideSymptom → Immediate Action4 entries
Symptom · 01
Blank page with no console errors
Fix
Check if error boundary exists at root. If yes, inspect the fallback UI — likely the boundary itself is broken.
Symptom · 02
Error boundary fallback shows but then crashes again
Fix
Wrap the boundary fallback in another boundary or ensure the fallback component doesn't throw. Use a static fallback with no state.
Symptom · 03
Partial UI disappears, rest works
Fix
Locate the affected subtree. The boundary that wraps it is working as expected. Look at the error logged via componentDidCatch to identify the culprit.
Symptom · 04
Error only occurs in production, not local dev
Fix
Check minified stack traces. Use source maps. Enable error monitoring (Sentry/Datadog) to capture the original error and component stack.
★ React Error Boundary Debug Cheat SheetQuick commands and checks to diagnose error boundary problems fast.
Blank screen, no error boundary fallback
Immediate action
Open browser console immediately
Commands
Look for uncaught errors in console — if none, React swallowed it silently
window.__REACT_DEVTOOLS_GLOBAL_HOOK__: check if React is mounted
Fix now
Wrap your root component in a global error boundary with a simple fallback like <div>Something went wrong</div>
Error boundary fires but fallback is blank+
Immediate action
Check the fallback component itself for errors
Commands
Render the fallback component directly outside the boundary to test
Add console.log inside componentDidCatch to verify error info
Fix now
Replace the fallback with a static JSX element (no state, no hooks) until the issue is understood
Only some users see the fallback+
Immediate action
Check for browser-specific errors (e.g., IE11, Safari)
Commands
Enable full source maps in production build
Use error logging service to group errors by browser and component stack
Fix now
Add a try-catch around the problematic third-party library call inside render, or wrap it in an isolated boundary
Error Handling in React: Options Compared
MechanismScopeCan Catch Render Errors?Can Catch Async Errors?Production Use Case
Error Boundary (class component)Child component treeYesNoWrap widgets, routes, third-party libraries
try-catch in event handlerThat handler onlyNoYes (if inside handler)Handle failed API calls, form submissions
window.onerror / global error handlerWhole pageYes (but no component stack)YesLast-resort logging, cannot show fallback UI
unhandledrejection eventPromise rejectionsNoYesCatch rejected Promises outside render

Key takeaways

1
Error boundaries catch render-time errors only
use them to isolate crashing components.
2
Place boundaries strategically
global for safety, granular for resilience.
3
Always log errors via componentDidCatch to your monitoring service.
4
Add retry logic with a cap to give users a way to recover.
5
Test your boundaries with a throwing child and ensure the fallback is self-contained.

Common mistakes to avoid

4 patterns
×

Placing a single error boundary at the root level

Symptom
Any widget crash shows a full-page error, making the entire app unusable instead of isolating the broken piece.
Fix
Use multiple granular boundaries around independent parts of the UI (sidebar, main content, header). Keep the root boundary as a last resort.
×

Not logging errors in componentDidCatch

Symptom
Users report blank areas but you have no logs or stack traces to debug the cause.
Fix
Always call your error monitoring service inside componentDidCatch with the error and component stack.
×

Forgetting that error boundaries don't catch async errors

Symptom
A useEffect that fetches data fails, but the error boundary never shows a fallback — the component renders with missing data.
Fix
Wrap the fetch call in a try-catch inside the effect, and use state to trigger the fallback manually.
×

Using error boundaries to catch errors in event handlers

Symptom
An onClick handler throws, but the boundary doesn't catch it — the app crashes silently.
Fix
Use try-catch directly inside the event handler and show a toast or inline error.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how React error boundaries work and what they can and cannot cat...
Q02SENIOR
How would you test an error boundary component?
Q03SENIOR
What is the difference between getDerivedStateFromError and componentDid...
Q01 of 03SENIOR

Explain how React error boundaries work and what they can and cannot catch.

ANSWER
Error boundaries are class components that implement either getDerivedStateFromError (to render a fallback) or componentDidCatch (to log side effects). They catch errors thrown during rendering, lifecycle methods, and constructors of the child tree. They do NOT catch errors in event handlers, async code (setTimeout, Promises, async/await), or during server-side rendering. They also cannot catch errors thrown in the boundary component itself. For async errors, you need try-catch inside the handler or a global rejection handler.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the simplest way to add an error boundary to an existing app?
02
Can I use hooks to create an error boundary?
03
How do I handle errors in event handlers that fire before the component renders?
04
Does the order of multiple error boundaries matter?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's React.js. Mark it forged?

4 min read · try the examples if you haven't

Previous
React Lifecycle Methods
16 / 47 · React.js
Next
React Suspense and Lazy Loading