react-error-boundary
React only lets class components catch render errors, and writing that class yourself gets you a boundary that can show a fallback but cannot easily be reset, cannot be triggered from an event handler, and has no hook API. react-error-boundary is that class component written once, properly, plus the pieces around it: three ways to declare a fallback, a resetErrorBoundary callback handed to the fallback so the user can retry, a resetKeys prop that clears the error automatically when some piece of state changes, and a useErrorBoundary hook whose showBoundary lets you push an error from a click handler or an async callback into the nearest boundary. It has no runtime dependencies, weighs under a kilobyte gzipped, and works with any React renderer including React Native.
The default answer for error boundaries in React, small enough that the decision is not worth agonising over, and the reset and showBoundary APIs are the parts you would otherwise write badly yourself. Just go in knowing it catches render errors only, which is a React rule and not something this package can change.
Use it if
- You want a retry story rather than a dead screen: the fallback gets resetErrorBoundary, and resetKeys clears the boundary on its own when the route id or query parameter that caused the failure changes
- You need to surface errors that React will never catch on its own, such as a failed fetch inside an onClick handler, by calling showBoundary from the useErrorBoundary hook
- You want one place to report render errors, since onError receives the thrown value plus React's component stack and can hand both to your logging service
- You are wrapping several independent regions of a page so one broken widget does not take down the whole route, and you want each region's fallback written inline
- You are on a non-DOM renderer such as React Native, where framework-level error pages do not exist
- You expect it to catch the errors that actually break your app. React error boundaries do not catch anything thrown in event handlers, in setTimeout or promise callbacks that run after render, during server-side rendering, or inside the boundary component itself. This package inherits every one of those limits, so a rejected fetch in a submit handler still reaches window.onunhandledrejection unless you route it through showBoundary by hand
- Your error reporting SDK already ships a boundary. @sentry/react, @bugsnag/plugin-react, and @rollbar/react all include one that reports automatically. Running both means two implementations in the tree and remembering to call the reporter yourself from onError
- You are on a runtime or bundler without ES module support. Version 6 is ESM, and the README tells those projects to stay on version 5. This bites hardest in Jest setups that do not transform node_modules
- You only need a single boundary at the app root with a static message. That is a twenty-line class component with getDerivedStateFromError and no dependency at all; the value here is resetKeys, showBoundary, and the fallback render prop, so if you use none of them you are installing a package for nothing
- You are in a React Server Components app expecting it to cover server errors. It is a client component, so it needs a "use client" file and it cannot see anything that fails during server rendering. Next.js already gives you error.tsx and global-error.tsx for route-level boundaries
- You are still on React 17 or earlier. The peer range is ^18.0.0 || ^19.0.0 and nothing older is supported
Setup reality
The install is one command with no runtime dependencies and a peer dependency on React 18 or 19. Three things then catch people out. Version 6 is published as ES modules, and the README says projects on runtimes that cannot load ESM should stay on version 5; a Jest config that does not transform this package will throw on import. In any React Server Components framework the file holding the boundary needs a "use client" directive, and the props you pass across the boundary have to be serializable. And in development the error still appears: React logs the caught error to the console and the Vite or Next.js overlay still covers the screen, which routinely convinces people the boundary is not working when it is; close the overlay and the fallback is there. On the TypeScript side, the one error you will hit is "ErrorBoundary cannot be used as a JSX component", which is a duplicate @types/react in the tree rather than a bug here, and the documented fix is an overrides or resolutions pin. You must also pass exactly one of fallback, fallbackRender, or FallbackComponent; the types mark the other two as never, so passing two is a compile error rather than a silent precedence rule.
Patterns
Wrap a subtree with a retryable fallbackbasic-boundary
"use client";
import { ErrorBoundary, getErrorMessage } from "react-error-boundary";
export function Page() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div role="alert">
<p>Something went wrong: {getErrorMessage(error)}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Dashboard />
</ErrorBoundary>
);
}Pass exactly one of fallback, fallbackRender, or FallbackComponent; the types declare the other two as never. In development React still logs the error and your dev overlay still appears over the fallback, which is expected and does not happen in a production build.
Extract the fallback into a typed componentfallback-component
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<pre>{String(error)}</pre>
<button onClick={resetErrorBoundary}>Reload section</button>
</div>
);
}
<ErrorBoundary FallbackComponent={Fallback}>
<Widget />
</ErrorBoundary>;FallbackProps types error as unknown rather than Error, because JavaScript lets you throw anything. Narrow it before reading .message, or call getErrorMessage, which returns undefined for values that have no message.
Clear the error automatically when state changesreset-keys
<ErrorBoundary
resetKeys={[userId, filter]}
onReset={(details) => {
if (details.reason === "keys") {
// reset caused by resetKeys changing
} else {
// reason === "imperative-api": user clicked retry
}
}}
FallbackComponent={Fallback}
>
<UserReport userId={userId} filter={filter} />
</ErrorBoundary>resetKeys is compared by reference on each render, so an inline array or object literal in that list resets the boundary on every render and hides the error immediately. Keep the entries to primitives or memoized values.
Send an event handler or async error to the boundaryasync-error
import { useErrorBoundary } from "react-error-boundary";
function SaveButton() {
const { showBoundary } = useErrorBoundary();
async function onClick() {
try {
await save();
} catch (error) {
showBoundary(error);
}
}
return <button onClick={onClick}>Save</button>;
}This is the workaround for React's biggest boundary limitation: nothing thrown from an event handler or a promise settled after render reaches a boundary on its own. The hook only works inside an ErrorBoundary subtree, so a component rendered above the boundary gets nothing.
Log every caught error with its component stackreport-errors
<ErrorBoundary
fallback={<p>This section is unavailable.</p>}
onError={(error, info) => {
logger.error("render_error", {
message: getErrorMessage(error),
componentStack: info.componentStack,
});
}}
>
<Chart />
</ErrorBoundary>onError fires from componentDidCatch, so it runs after React has already committed the fallback. info.componentStack is the only place you get the React tree path, and it is far more useful than the JavaScript stack when the error came from a shared component.
Handle only some errors and pass the rest upwardrethrow-unhandled
function Fallback({ error, resetErrorBoundary }: FallbackProps) {
if (!(error instanceof ChartDataError)) {
throw error; // bubbles to the parent boundary
}
return <button onClick={resetErrorBoundary}>Reload chart</button>;
}A boundary cannot catch an error thrown from its own fallback, so rethrowing there hands the error to the next boundary up. That is how you build a specific widget boundary underneath a generic app-level one without the widget swallowing unrelated failures.
Wrap an existing component without touching its JSXhoc-wrapper
import { withErrorBoundary } from "react-error-boundary";
const SafeChart = withErrorBoundary(Chart, {
fallback: <p>Chart failed to load.</p>,
onError: reportError,
});Useful for components rendered from a registry or a config-driven layout where you never write their JSX by hand. It forwards refs, but the boundary configuration is fixed at wrap time, so anything that has to depend on props needs the component form instead.
Isolate regions so one failure does not blank the pagegranular-boundaries
<Layout>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedError />} resetKeys={[feedId]}>
<Feed feedId={feedId} />
</ErrorBoundary>
</Layout>Boundaries only catch below themselves, so the placement is the design. One boundary at the root turns any component error into a blank app; one per independently loaded region keeps the rest of the page alive and gives the user something to retry.
Combine with Suspense for loading and failuresuspense-and-boundary
<ErrorBoundary FallbackComponent={Fallback} resetKeys={[queryKey]}>
<Suspense fallback={<Skeleton />}>
<AsyncList queryKey={queryKey} />
</Suspense>
</ErrorBoundary>Order matters: the boundary must sit outside Suspense, otherwise a thrown promise reaches the boundary before Suspense sees it. When the retry needs to refetch, wire resetKeys or onReset to whatever cache your data layer uses, since resetting the boundary alone just re-renders the same failed state.
Let a React 19 transition route the error for youtransition-actions
function AddComment() {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() =>
startTransition(async () => {
await addComment(); // a rejection reaches the nearest boundary
})
}
>
{isPending ? "Adding..." : "Add comment"}
</button>
);
}On React 19 an error thrown inside the function passed to startTransition is caught by the nearest boundary, so this replaces the manual showBoundary call for actions. It does nothing for plain event handlers that are not wrapped in a transition.
Get a message out of a value that may not be an Errorread-thrown-value
import { getErrorMessage } from "react-error-boundary";
const message = getErrorMessage(error) ?? "Unexpected error";Third-party code and some transpiled async paths throw strings, plain objects, or promise rejection values. getErrorMessage returns string | undefined instead of assuming error.message exists, which stops the fallback itself from throwing and escalating to the parent boundary.
Fix ErrorBoundary cannot be used as a JSX componenttypes-react-mismatch
// package.json (npm)
{
"overrides": { "@types/react": "19.0.0" }
}
// package.json (yarn)
{
"resolutions": { "@types/react": "19.0.0" }
}This TypeScript error means two different @types/react versions resolved in your tree, usually dragged in by another component library, and pinning one version fixes it. Run npm ls @types/react first to confirm that is what you are looking at.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @sentry/react | npm | You already send errors to Sentry and would rather have one component that both renders a fallback and reports the error with the release and user context attached |
| @bugsnag/plugin-react | npm | Bugsnag is your reporting backend and you want its boundary wired to the same client instance |
| @rollbar/react | npm | Rollbar is your reporting backend and you want its provider plus boundary rather than a standalone one |