Every React app eventually hits a render that throws. A component reads a property off undefined, a malformed API response slips past validation, or a third-party widget loses its mind. Without a safety net, one bad render takes down your entire tree and leaves the user staring at a blank page. React's answer to this is the error boundary — a component that catches errors thrown while rendering its children and shows a fallback instead of crashing everything.
The catch is that React only ships error boundaries as a class component API, built around componentDidCatch and static getDerivedStateFromError. There's no hook, no render prop, no declarative reset. That's exactly the gap react-error-boundary fills. Written by Brian Vaughn (former React core team member and author of react-window and the React DevTools profiler), it's a tiny, dependency-free, beautifully typed <ErrorBoundary> that you drop in once and never think about writing by hand again. With roughly 13 million weekly downloads, it's quietly one of the most depended-on utilities in the React ecosystem.
Why Reach for It
The library is small but covers the parts of error handling that hurt the most:
- A declarative
<ErrorBoundary>component with three different ways to render a fallback, so you can match the ergonomics to the situation. - Recovery built in — reset the boundary imperatively from a "Try again" button, or declaratively whenever a piece of state changes.
- An escape hatch for the errors boundaries normally miss — event handlers and async callbacks — through the
useErrorBoundaryhook. - Honest TypeScript — the thrown value is typed as
unknown(because JavaScript can throw anything), with agetErrorMessagehelper to read it safely. - Zero runtime dependencies and a tiny footprint, with peer support for React 18 and 19.
Getting It Into Your Project
Installation is a single package with no extra setup:
npm install react-error-boundary
yarn add react-error-boundary
A quick note on versions: v6 is ESM-first and aimed at modern toolchains. If you're stuck in an environment without ES Module support, the v5 line is the recommended fallback. Everything below targets the current v6 API.
Three Ways to Show a Fallback
The single most useful thing to understand about <ErrorBoundary> is that it gives you three mutually exclusive ways to render a fallback, and TypeScript enforces that you pick exactly one. Each suits a different level of complexity.
The Quick One: a Static Fallback
When you just want to swap in some JSX and don't need the error details, use the fallback prop:
"use client";
import { ErrorBoundary } from "react-error-boundary";
export function Widget() {
return (
<ErrorBoundary fallback={<div role="alert">This widget is taking a nap.</div>}>
<FlakyChart />
</ErrorBoundary>
);
}
This is perfect for non-critical, self-contained pieces of UI — a sidebar widget, an embedded chart, an avatar — where the rest of the page should carry on as if nothing happened.
The Reusable One: a Fallback Component
When you want a consistent error UI across your app, write a component that receives FallbackProps and pass it as FallbackComponent:
"use client";
import { ErrorBoundary, getErrorMessage } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
function ErrorCard({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="error-card">
<p>Something went wrong.</p>
<pre>{getErrorMessage(error)}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function Dashboard() {
return (
<ErrorBoundary FallbackComponent={ErrorCard}>
<Reports />
</ErrorBoundary>
);
}
Notice getErrorMessage(error). In v6 the error prop is typed as unknown rather than Error, which is the honest type — JavaScript lets you throw a string, a number, or anything else. getErrorMessage does the narrowing for you: it returns the string itself if a string was thrown, the .message if it's an error-like object, and undefined otherwise. No manual type guards required.
The Flexible One: a Render Prop
When the fallback needs access to surrounding scope or you'd rather inline it, fallbackRender is a render-prop function:
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div role="alert">
<p>We couldn't load your data:</p>
<pre>{getErrorMessage(error)}</pre>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Profile />
</ErrorBoundary>
Functionally it's the same as FallbackComponent, but it keeps the fallback close to where it's used and can close over local variables without prop drilling.
Logging What Broke
A fallback handles the user-facing side, but you usually want to know an error happened too. The onError prop is your reporting hook — it fires with the thrown value and React's component stack:
<ErrorBoundary
FallbackComponent={ErrorCard}
onError={(error, info) => {
// send to Sentry, LogRocket, your own endpoint, etc.
reportError(error, info.componentStack);
}}
>
<Checkout />
</ErrorBoundary>
This is the pattern most teams settle on: react-error-boundary for the UI and recovery, plus an onError that ships the details off to a monitoring service. You get the ergonomic component and your existing observability stack, with no conflict between them.
Bouncing Back: Recovery Patterns
Showing a fallback is only half the job. The other half is letting the app recover without a full page reload. The library gives you two complementary ways to reset a boundary.
Imperative Reset from the Fallback
Every fallback receives resetErrorBoundary. Calling it clears the boundary's error state and re-renders the children — the classic "Try again" button. Pair it with onReset to clean up whatever state caused the trouble in the first place:
<ErrorBoundary
FallbackComponent={ErrorCard}
onReset={(details) => {
// details is a discriminated union in v6
if (details.reason === "imperative-api") {
// user clicked "Try again" — clear stale state, refetch, etc.
queryClient.invalidateQueries(["reports"]);
}
}}
>
<Reports />
</ErrorBoundary>
In v6 the onReset callback receives a structured details object so you can tell why a reset happened: { reason: "imperative-api", args } when someone called resetErrorBoundary(...), or { reason: "keys", prev, next } when reset keys changed.
Declarative Reset with resetKeys
Sometimes the error is tied to a specific input, and you want the boundary to recover automatically the moment that input changes. That's what resetKeys is for — pass an array, and whenever any value in it changes, the boundary resets itself:
function UserPanel({ userId }: { userId: string }) {
return (
<ErrorBoundary FallbackComponent={ErrorCard} resetKeys={[userId]}>
<UserDetails userId={userId} />
</ErrorBoundary>
);
}
If loading user 42 throws, the fallback shows. The instant the user navigates to user 43, the changed userId in resetKeys clears the boundary and tries the fresh render automatically. No retry button required — the change of context is the retry. This is the cleaner choice whenever an error is logically bound to some piece of state like a route param, a selected id, or a filter.
The Errors Boundaries Can't See
Here's the gotcha that trips up nearly everyone: error boundaries only catch errors thrown during rendering of the components below them. They do not catch:
- Errors in event handlers (a click handler that throws).
- Errors in async code —
setTimeout, unresolved promises, afetch().then()that rejects. - Errors in the boundary component itself.
- Errors during server-side rendering.
This makes sense once you internalize it: by the time your click handler runs or your promise resolves, React is done rendering and the boundary isn't watching anymore. The fix is the useErrorBoundary hook, which lets you forward a caught error to the nearest boundary on purpose:
"use client";
import { useErrorBoundary } from "react-error-boundary";
import { useEffect } from "react";
function LiveData() {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetchData().catch((err) => showBoundary(err));
}, [showBoundary]);
return <Chart />;
}
You catch the async error yourself, then call showBoundary(err) to surface it through the same fallback UI you already built. The hook returns { error, resetBoundary, showBoundary } and must be called inside an <ErrorBoundary> subtree. (If you've used older versions, this replaced the v3-era useErrorHandler hook.)
There's a nice bit of good news here too: in React 19, errors thrown from a function passed to startTransition — Actions and transition-driven updates — are caught by the nearest boundary automatically. That narrows the gap, so for transition-based flows you often don't need the imperative hook at all.
Wrapping Without the Nesting
If you'd rather not wrap JSX by hand every time, the withErrorBoundary higher-order component bakes a boundary right into a component:
import { withErrorBoundary } from "react-error-boundary";
const SafeProfile = withErrorBoundary(Profile, {
FallbackComponent: ErrorCard,
onError(error, info) {
reportError(error, info.componentStack);
},
});
// Use <SafeProfile /> anywhere — it's already protected.
This is handy for components you always want guarded, or when you're protecting something deep in a tree where adding another layer of JSX nesting would be noisy.
The Next.js Reality Check
If you work in the Next.js App Router, there's one boundary worth setting straight, because it's the most common mistake people make with this library: react-error-boundary cannot catch errors thrown inside Server Components.
The <ErrorBoundary> here is a Client Component — it carries the "use client" directive, and it only sees client-side render errors. Server Component rendering happens on the server, where a client React boundary never gets a chance to catch anything. Next.js instead serializes server errors and routes them to its own file-convention boundaries: error.tsx per route segment, and global-error.tsx at the root. (In production, Next.js also scrubs server error details before sending them to the client, so a generic message there is expected, not a bug.)
The healthy way to think about it: these tools are complementary, not competitors.
- Use
react-error-boundaryfor client-side concerns — interactive widgets, client-side data fetching, event and async errors viashowBoundary, and granular in-page recovery withresetKeysandresetErrorBoundary. - Use Next.js's
error.tsx/global-error.tsxfor route-level and server-rendering errors.
Reaching for react-error-boundary to "catch server component errors" is the trap. It can't, by design — and once you know that, both tools become easy to place.
Wrapping Up
react-error-boundary earns its enormous download count by doing one thing extremely well: turning React's clunky class-only error boundary into a small, ergonomic, fully-typed component you'll actually enjoy using. Pick a fallback strategy that fits the situation, lean on resetKeys and resetErrorBoundary for recovery, reach for useErrorBoundary().showBoundary when errors slip outside the render path, and pair an onError with your monitoring tool of choice. Keep it on the client where it belongs, let the framework handle server and route concerns, and your app stops treating a single bad render as a fatal event. That's a safety net worth the 49 KB.