
Next.js catchError
error.js catches errors for an entire route segment — but not every error boundary you need actually lines up with a route segment boundary. Sometimes you want error recovery scoped to one specific widget, one card in a dashboard, one section of a page that has nothing to do with how your file system is organized into routes. catchError is Next.js's answer: a programmatic function that builds a proper error boundary — with the framework's own recovery semantics baked in — around any part of your component tree, independent of route structure entirely.
This is a genuinely new, recently-stabilized API (stable as of Next.js 16.3.0), and it's worth understanding both what it gives you over a hand-rolled React error boundary and exactly how it composes with error.js rather than replacing it.
Why Not Just Write a Custom React Error Boundary?
You certainly could — error boundaries are a standard React pattern with getDerivedStateFromError and componentDidCatch. But catchError gives you three things a hand-rolled boundary doesn't provide automatically:
- Built-in error recovery. Its
retry()re-renders the page inside a React Transition, specifically preserving Client Component state that lives outside the error boundary — a hand-rolled boundary's naive re-render wouldn't give you that preservation for free. - Framework-aware integration. Next.js APIs like
redirect()andnotFound()work by throwing special, framework-recognized errors internally. A generic error boundary would catch these the same as any other thrown error — which is exactly wrong, since anotFound()call is supposed to trigger routing behavior, not get swallowed as a generic error.catchErroralready knows to let these pass through correctly. - Client navigation handling. The error state automatically clears the moment the user navigates client-side to a different route — you don't have to manually reset boundary state on route change yourself.
Basic Usage
"use client";
import { catchError, type ErrorInfo } from "next/error";
function ErrorFallback(props: { title: string }, { error, retry }: ErrorInfo) {
return (
<div>
<h2>{props.title}</h2>
<p>{error.message}</p>
<button onClick={() => retry()}>Try again</button>
</div>
);
}
export default catchError(ErrorFallback);
catchError can be called from Client Components — and the fallback function you pass it must itself be a Client Component (or defined in a 'use client' module), the same requirement error.js has for the same underlying reason: error boundaries are a client-only React feature.
The fallback Function's Two Arguments
const ErrorWrapper = catchError(fallback);
fallback receives:
props— whatever props are passed to the wrapper component you get back fromcatchError, excludingchildren.errorInfo— an object with three fields:
| Property | Type | Description |
|---|---|---|
error | Error | The caught error instance |
retry | () => void | Re-fetches and re-renders the boundary's children; replaces the fallback with the successful result if it works |
reset | () => void | Resets the error state and re-renders without re-fetching |
Exactly the same guidance as error.js applies here: use retry() in most cases, since reset() only clears state without re-running the failed work — meaning it won't recover from a Server Component error, only from certain client-side error states where nothing actually needs re-fetching.
What You Get Back
catchError returns a React component that accepts the same props your fallback's first argument expects, plus children — it wraps children in an error boundary and renders fallback if anything inside throws.
Basic Wiring
import ErrorWrapper from "../custom-error-boundary";
export default function Component({ children }: { children: React.ReactNode }) {
return <ErrorWrapper title="Dashboard Error">{children}</ErrorWrapper>;
}
The title prop here flows straight through to ErrorFallback's props argument — this is exactly what makes the pattern reusable: build one error-boundary component, wrap it around many different sections of your app, and vary its displayed content per usage through ordinary props.
Server-Rendered Fallback Content
A more advanced pattern: pass genuinely server-rendered content as a React.ReactNode prop, so the fallback UI itself can reflect live, fetched data rather than a static message:
"use client";
import { catchError, type ErrorInfo } from "next/error";
function ErrorFallback(
props: { fallback: React.ReactNode },
errorInfo: ErrorInfo,
) {
return props.fallback;
}
export default catchError(ErrorFallback);
import ErrorBoundary from "../error-boundary";
async function ErrorFallback() {
const data = await getData();
return <div>{data.message}</div>;
}
export default function Component({ children }: { children: React.ReactNode }) {
return <ErrorBoundary fallback={<ErrorFallback />}>{children}</ErrorBoundary>;
}
Worth being deliberate about before reaching for this: the docs explicitly flag that this pattern eagerly renders the fallback content on every single page render, whether or not an error actually occurs — since the fallback prop's JSX has to be constructed regardless of whether the boundary ever triggers it. For most use cases, a simpler client-side-only fallback (the basic pattern above) is genuinely sufficient, and this data-driven fallback pattern is worth reserving for cases where the error UI specifically needs to reflect real, current data rather than a generic message.
catchError vs. error.js — Where Each One Fits
error.js | catchError | |
|---|---|---|
| Scope | An entire route segment | Any arbitrary part of the component tree |
| Placement | Fixed file-system location per segment | Wrapped around whatever component you choose, wherever it lives |
| Built-in Next.js error boundary | Yes — automatically | No — you build the wrapper yourself, once, and reuse it |
Critically: you do not need to wrap an error.js default export in catchError. error.js already renders inside a built-in error boundary Next.js provides automatically at the file-convention level — layering catchError on top of it would be redundant, not additive. Reach for catchError specifically for component-level error recovery that doesn't map cleanly onto a route segment boundary — a single dashboard widget, one card in a grid of independently-fetched cards — where error.js's route-segment granularity is simply too coarse for what you actually need.
Version History
| Version | Changes |
|---|---|
v16.3.0 | catchError became stable |
v16.2.0 | unstable_catchError introduced |
Key Takeaways
| Aspect | Detail |
|---|---|
| Purpose | Programmatic, component-level error boundaries — independent of route-segment structure |
| Framework awareness | Correctly passes through redirect()/notFound()'s internal special errors instead of catching them as generic errors |
retry() vs reset() | Prefer retry() — it genuinely re-fetches; reset() only clears state |
| Reusability | Props flow through to the fallback, making one wrapper reusable across many differently-titled/configured sections |
| Server-rendered fallback content | Powerful, but eagerly rendered on every page render — reserve for cases that genuinely need it |
Relationship to error.js | Complementary, not redundant — don't wrap error.js's own export in catchError |
catchError fills a real gap error.js structurally can't: error recovery that doesn't respect route boundaries. Reach for it when a single component — not an entire segment — is the right unit of failure isolation, and lean on its built-in retry() semantics rather than reinventing recovery logic a hand-rolled error boundary would otherwise leave you to build yourself.


