React error boundaries are one of those features that seem simple until you ship with them and discover what they actually do—and what they silently miss. A react error boundary is a React component that catches errors thrown by child components during rendering, lifecycle updates, and constructors. But a lot of code that throws doesn't happen in those phases, and boundaries can't help there. Understanding exactly when a boundary catches errors—and when it doesn't—is critical to building reliable React applications.
This guide covers what error boundaries actually catch, why they miss certain errors, how to implement one correctly in production, and how to report those caught errors to an error tracker like LightTrace.
What React Error Boundaries Catch (and Don't)
A react error boundary is a class component with special React lifecycle methods that let it intercept errors thrown by its descendants. But React's component lifecycle is narrower than you might think.
Errors a boundary CATCHES:
- Render phase errors — Any exception thrown in a component's JSX or render logic, before React finishes constructing the virtual tree.
- Lifecycle method errors — Exceptions in
componentDidMount,componentDidUpdate,componentWillUnmount, and other lifecycle hooks of child components. - Constructor errors — Errors thrown in a child component's
constructor. - Errors in a child's
getDerivedStateFromError— If that method itself throws.
Errors a boundary DOES NOT catch:
- Event handler errors — An exception thrown inside an
onClick,onSubmit, or any other event listener runs outside React's render cycle, so the boundary never sees it. Use a try/catch inside the handler, or set up a global handler withwindow.addEventListener('error', ...). - Async code — Errors in
setTimeout,setInterval, Promises,fetchcallbacks, and any code scheduled for later. React's error boundary only works during render; by the time a Promise rejects or a timeout fires, the render phase is over. Use.catch()on promises ortry/catchin async/await; for unhandled rejections, set up a global handler. - Server-side rendering — Error boundaries are a client-side React feature and don't work in SSR contexts. On the server, you need middleware-level error handling. See server-side rendering error tracking for the pattern.
- Errors thrown in the boundary itself — If the error boundary's own
getDerivedStateFromError,componentDidCatch, or render method throws, the boundary can't catch its own error. The error propagates up to a parent boundary (or crashes the app if there is none). - Errors in ancestor components — A boundary only catches errors in its descendants, not in components above it or in sibling trees.
Why the boundary can't see event handlers and async code
React's error boundary hooks into the render phase of the component lifecycle. When you throw inside a component's JSX, React catches it at the moment it's trying to build the virtual DOM. But when an event fires (e.g., a button click) or a timer goes off, that code runs outside the render phase, in the browser's event loop. React has no way to intercept those exceptions because they're not happening during a render.
A Correct Error Boundary Implementation
Here's a production-ready error boundary using both getDerivedStateFromError and componentDidCatch:
import React from "react";
interface ErrorBoundaryState {
hasError: boolean;
errorMessage: string;
componentStack?: string;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = {
hasError: false,
errorMessage: "",
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
// Update state so the next render shows the fallback UI
return {
hasError: true,
errorMessage: error.message,
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Log to your error tracker (e.g., LightTrace via Sentry SDK)
console.error("Error caught by boundary:", error, errorInfo);
// Store the component stack for debugging
this.setState({
componentStack: errorInfo.componentStack,
});
// Report to LightTrace
if (window.__SENTRY__) {
window.__SENTRY__.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
}
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: "2rem" }}>
<h2>Something went wrong</h2>
<details style={{ whiteSpace: "pre-wrap", fontSize: "0.85rem" }}>
<summary>{this.state.errorMessage}</summary>
{this.state.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
The division of labor is crucial:
getDerivedStateFromError— Called immediately when an error is caught. It's a pure static method, so it can only return new state to render a fallback UI. It doesn't have access tothisorerrorInfo.componentDidCatch— Called after the fallback UI is rendered. This is where you log, report to your error tracker, and accesserrorInfo.componentStack, which is the tree of components that led to the error. It's the one piece of data that makes React errors readable.
Many tutorials skip componentDidCatch, but it's where the real value lives: errorInfo.componentStack tells you which components were in the render tree, making it trivial to reproduce the error.
Error boundaries only work with class components. There is no hook-based error boundary. If you want to use error boundaries in a functional component-heavy codebase, the community library react-error-boundary wraps a class boundary and exposes it via a hook. (It's not the same as the framework's built-in boundary, but it works the same way.)
Where to Place Error Boundaries
A single error boundary at the app root feels safe, but it's a trap. If one render error anywhere in your app throws, the entire UI disappears.
Better strategy:
- Route-level boundaries — Wrap each top-level route. A crash on one page doesn't blank the whole app.
- High-risk subtrees — Wrap complex widgets like editors, forms, or library integrations that might throw during render.
- Combine them — A top-level boundary as a last resort, route boundaries for isolation, and spot boundaries around risky components.
This way, an error in the admin panel doesn't take down the user dashboard, and a bad data render in one widget doesn't unmount your sidebar.
If you're using React Router, wrap your route element with an error boundary so each route has its own error handler. That's often enough to keep your app standing.
Resetting After an Error
Once an error boundary catches an error, it stays in the error state and keeps showing the fallback UI. The user is stuck unless you give them a way to recover.
Patterns for resetting:
-
keyprop — Pass akeyto the boundary itself. When the key changes, React unmounts and remounts the boundary, clearing the error state.const [resetKey, setResetKey] = useState(0); return ( <> <ErrorBoundary key={resetKey}> <App /> </ErrorBoundary> <button onClick={() => setResetKey(k => k + 1)}> Try again </button> </> ); -
react-error-boundary'sresetKeys— If you use that library, pass an array of props that, when changed, automatically resets the boundary. -
Manual reset method — Store a ref to the boundary and call a reset method on it (less common).
A boundary without a reset path traps the user. Always pair a boundary with a recovery mechanism.
React 19 Changes to Error Handling
React 19 introduced two root-level error handling options in your createRoot call:
const root = createRoot(document.getElementById("root"), {
onUncaughtError: (error, errorInfo) => {
// Called for errors that escape all error boundaries
console.error("Uncaught error:", error, errorInfo.componentStack);
},
onCaughtError: (error, errorInfo) => {
// Called when an error boundary catches an error
console.error("Caught error:", error, errorInfo.componentStack);
},
});
These aren't replacements for error boundaries—they're reporting hooks. Use onCaughtError to send every caught error to your error tracker without cluttering componentDidCatch in every boundary. Use onUncaughtError to catch errors that somehow escaped all boundaries.
These APIs let you centralize error reporting across your whole app. Combine them with LightTrace for automatic issue grouping and stack-trace deobfuscation.
Reporting Caught Errors to Your Error Tracker
A caught error is worthless if no one sees it. In production, report it to your error tracker. If you're using the Sentry SDK with LightTrace, call it from componentDidCatch:
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
import("@sentry/react").then((Sentry) => {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
});
}
The componentStack is critical—it's the component tree that led to the error, and it's what makes the error meaningful in your dashboard. Without it, you just have a stack trace and a mystery.
In development, React double-invokes your component and its error boundary in StrictMode, so you'll see errors reported twice. That's intentional (it finds bugs early) but can look alarming in your error tracker. You can filter them out by environment tag if it gets noisy.
Summary: Boundaries Are Not Magic
Error boundaries are a powerful tool for keeping your React app's UI from completely white-screening when a component crashes during render. But they're only one part of error monitoring. They don't catch event handlers, async errors, or errors outside the render phase—you still need try/catch and global handlers for those. Combine error boundaries with global JavaScript error handling to cover everything, and report all of it to your error tracker so you actually know when your app is broken.
Place boundaries strategically (route level + risky widgets), make them report errors, and always give users a way to recover.
Start tracking errors in minutes
Start tracking React render errors with automatic error grouping and component-stack readability — point the Sentry SDK at LightTrace and move faster.
For the full SDK setup guide, including source maps and breadcrumb tracing, see how to add error tracking to React. For a deeper look at what can go wrong in JavaScript beyond the React layer, read JavaScript error monitoring.