An error boundary is a React component that catches errors thrown while rendering the components inside it and shows a fallback UI instead of crashing the whole app. It has to be a class component that defines static getDerivedStateFromError, because there is no hook for it. You write it once and wrap it around any part of the tree.
Press "Break the profile" to make the child throw during render.
The profile is replaced by the fallback, and the rest of the page keeps working. The console shows the error twice: React logs every caught error with console.error, and componentDidCatch logs it again. "Try again" clears the cause (broken) and the boundary's error, so the profile renders again. Remove this.props.onReset(); from reset and Try again throws straight back into the fallback, because nothing fixed the cause.
How the class works
Two methods make a class an error boundary:
static getDerivedStateFromError(error)runs during rendering, right after a child throws. It returns the state update that switches the boundary to its fallback. It must be pure: no logging, no requests.componentDidCatch(error, info)runs after the fallback is on screen. Use it for side effects such as sending the error to a reporting service.info.componentStacklists the component that threw and the components above it.
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // switch to the fallback
}
componentDidCatch(error, info) {
reportError(error, info.componentStack); // your logging service
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
There is no hook equivalent, so even an app written only with function components needs this one class. The class components page explains the class syntax if it is new to you.
What a boundary catches, and what it does not
A boundary catches errors that happen while React is working on the components inside it:
- rendering (a function component's body, a class's
render), - lifecycle methods and constructors of child class components,
- effects (
useEffect,useLayoutEffect) and ref callbacks, - a rejected promise read with use, and in React 19 an error thrown by a form action or by a function passed to the
startTransitionfromuseTransition.
It does not catch:
- errors in event handlers,
- async code you start yourself:
setTimeout,.then()callbacks, anawaitin an event handler, - errors in the boundary's own
render(the next boundary up catches those), - errors during server rendering.
Event handlers are left out because they do not break rendering. The screen is still in a valid state, so a try/catch in the handler is enough. If you do want the boundary to show, store the error in state and throw it on the next render:
The first button leaves the buttons on screen and only prints an uncaught error in the console. The second catches the rejection, stores it, and the boundary takes over.
Where to put boundaries
One boundary around the whole app stops the white screen, but then any small error replaces everything. Put boundaries around parts that can fail independently: a sidebar widget, a chart, each route. A failure then costs the user one panel, not the page.
Only the Stocks panel shows its fallback. Move both widgets into one <ErrorBoundary> and the working Weather panel disappears with it.
Without any boundary, React removes the whole tree from the screen when a render error is not caught. An empty page is safer than a broken one that shows wrong data, but users read it as a crash.
The react-error-boundary library
The react-error-boundary package wraps the class for you and adds reset helpers. It is not available in this editor, so here it is in a static block:
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={Fallback} onReset={() => refetch()} resetKeys={[userId]}>
<Profile userId={userId} />
</ErrorBoundary>;
// Inside a child: hand an async error to the boundary
const { showBoundary } = useErrorBoundary();
fetchData().catch(showBoundary);
resetKeys resets the boundary when any listed value changes, and showBoundary does the "store and throw" trick from the earlier example.
Reporting errors in React 19
React 19 reports errors in a new way. Errors caught by a boundary are logged once with console.error, and uncaught errors go to window.reportError. To send them to your own service, pass options to createRoot. Each option replaces the matching default logging:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
onCaughtError(error, errorInfo) {
// an error boundary caught it
logToService('caught', error, errorInfo.componentStack);
},
onUncaughtError(error, errorInfo) {
// no boundary caught it, and React removed the tree
logToService('uncaught', error, errorInfo.componentStack);
},
onRecoverableError(error, errorInfo) {
// React recovered by itself, for example after a hydration mismatch
logToService('recovered', error, errorInfo.componentStack);
},
});
root.render(<App />);
onRecoverableError is for errors React fixed without your help, such as a hydration mismatch or a render error that went away when React retried. hydrateRoot takes the same three options.
Common mistakes
Putting the boundary inside the component that throws. A boundary only catches errors from its children. If Profile renders <ErrorBoundary> around its own JSX and then throws in its own body, the error goes past it to the next boundary up. Wrap the component from its parent.
Side effects in getDerivedStateFromError. It runs during rendering and may run more than once, so keep it to returning state. Logging belongs in componentDidCatch or in the root's onCaughtError.
A reset that does not fix anything. Clearing the boundary's error renders the same children with the same props. If they throw again, the user is back on the fallback. Change what caused the error first (refetch, clear bad input, change the key), then clear the error, as the first example does with onReset.
One boundary for the whole app, and nothing else. It prevents the blank page but turns every small failure into a full-page error. Add boundaries around features, and keep the top-level one as the last resort.
Fallbacks that hide the error from you. A friendly message is right for users, but make sure the error still reaches your logs. React 19 logs caught errors to the console by default; in production that console is your user's, so send errors to a service from componentDidCatch or onCaughtError.
Error boundaries pair naturally with <Suspense>: Suspense handles "not ready yet", the boundary handles "failed". Put the boundary outside the Suspense, as in the use examples, so one component covers loading, success and failure.
Frequently Asked Questions
What is an error boundary in React?
A component that catches JavaScript errors thrown while rendering any component inside it, and renders a fallback UI instead. Without one, an error during render removes the whole app from the screen.
Is there a hook for error boundaries?
No. An error boundary must be a class component with static getDerivedStateFromError (and optionally componentDidCatch). You write it once and use it from function components like any other component, or install react-error-boundary, which wraps the class for you.
Do error boundaries catch errors in event handlers?
No. Event handlers run outside rendering, so React does not need a boundary to recover. Use try/catch in the handler, and if you want the boundary to show, store the error in state and throw it during render.
Do error boundaries catch async errors?
Not from setTimeout, promise callbacks or fetch().then() that you start yourself. They do catch a rejected promise read with use, and in React 19 an error thrown inside a form action or a function passed to the startTransition from useTransition.
How do I reset an error boundary?
Clear its error state, usually from a Try again button in the fallback, after fixing whatever caused the error. Changing the boundary's key also resets it, because React mounts a new instance.