ShipAI.today dev reference
React Error Boundary: 3 ways to catch errors in 2026.
React Error Boundaries prevent your app from crashing when a component throws. In 2026 you have three options: the raw class component pattern, the react-error-boundary library, and Next.js App Router's built-in error.tsx files. This guide covers all three with TypeScript code, explains what they catch (and don't), and tells you which to use.
Error catching scope
What React Error Boundaries catch (and don't catch)
Error Boundaries only catch rendering errors. Understanding the scope prevents bugs.
✅ Errors that ARE caught
❌ Errors that are NOT caught
Option 1 — Class component
Building an Error Boundary from scratch (TypeScript)
Error Boundaries must be class components. Here's the minimal production-ready implementation.
components/error-boundary.tsx
"use client"; // Required if used in Next.js App Router
import { Component, type ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
fallbackRender?: (props: { error: Error; reset: () => void }) => ReactNode;
onError?: (error: Error, info: { componentStack: string }) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
// Called during render when an error is thrown below
// Return value updates state — triggers fallback render
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
// Called after the component tree has "committed" the error
// Use for logging to Sentry / PostHog / etc.
componentDidCatch(error: Error, info: { componentStack: string }) {
this.props.onError?.(error, info);
console.error("Error Boundary caught:", error, info.componentStack);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
// Custom render function takes priority
if (this.props.fallbackRender) {
return this.props.fallbackRender({
error: this.state.error,
reset: this.reset,
});
}
// Static fallback node
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback
return (
<div className="p-4 text-center">
<p className="text-sm font-medium text-destructive">Something went wrong.</p>
<button
onClick={this.reset}
className="mt-2 text-xs text-muted-foreground underline"
>
Try again
</button>
</div>
);
}
return this.props.children;
}
}Usage examples
// 1. Simple fallback node
<ErrorBoundary fallback={<p>Widget failed to load.</p>}>
<ExpensiveWidget />
</ErrorBoundary>
// 2. Custom fallback with reset button
<ErrorBoundary
fallbackRender={({ error, reset }) => (
<div className="rounded border border-destructive p-4">
<p className="text-sm font-medium">Chart failed: {error.message}</p>
<button onClick={reset} className="mt-2 text-xs underline">
Retry
</button>
</div>
)}
onError={(error) => Sentry.captureException(error)}
>
<AnalyticsChart />
</ErrorBoundary>
// 3. Wrap entire sections independently
<>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
</>Wrapping sections independently means a crash in the Sidebar doesn't kill the MainContent — and vice versa. This is the main benefit of granular Error Boundary placement.
Option 2 — react-error-boundary
The react-error-boundary library (recommended for most apps)
The react-error-boundary package gives you a ready-made ErrorBoundary component and the useErrorBoundary() hook for catching async errors.
Install
bun add react-error-boundary
# or: npm install react-error-boundaryErrorBoundary component usage
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert" className="p-4 border border-destructive rounded-md">
<p className="text-sm font-medium text-destructive">Something went wrong</p>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
<button
onClick={resetErrorBoundary}
className="mt-3 text-xs rounded bg-primary text-primary-foreground px-3 py-1.5"
>
Try again
</button>
</div>
);
}
// Usage
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// Log to your error tracker
Sentry.captureException(error, { extra: info });
}}
onReset={() => {
// Optional: reset app state when user clicks "Try again"
queryClient.clear();
}}
>
<App />
</ErrorBoundary>useErrorBoundary() — throw async errors into the boundary
The killer feature: useErrorBoundary() lets you throw errors from async code and event handlers into the nearest Error Boundary. Without this, async errors bypass the boundary entirely.
"use client";
import { useErrorBoundary } from "react-error-boundary";
function DataFetchingComponent() {
const { showBoundary } = useErrorBoundary();
async function loadData() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setData(data);
} catch (error) {
// Throw the async error into the nearest ErrorBoundary
showBoundary(error);
}
}
// Event handler errors are also caught this way
function handleClick() {
try {
riskyOperation();
} catch (error) {
showBoundary(error);
}
}
return <button onClick={handleClick}>Load data</button>;
}Option 3 — Next.js App Router
Next.js error.tsx: the built-in Error Boundary
Next.js App Router has first-class Error Boundary support via error.tsx files. No library needed.
How error.tsx works
Place an error.tsx file in any route segment directory. Next.js automatically wraps the page.tsx in that segment with a React Error Boundary, using your error.tsx as the fallback UI.
The error.tsx receives two props: error: Error & { digest?: string } and reset: () => void — where reset re-renders the page.tsx segment.
app/dashboard/error.tsx
"use client"; // error.tsx MUST be a Client Component
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to your error tracking service
console.error("Dashboard error:", error);
// Sentry.captureException(error);
}, [error]);
return (
<div className="flex min-h-100 flex-col items-center justify-center gap-4 p-8 text-center">
<div className="rounded-full bg-destructive/10 p-4">
<svg className="h-8 w-8 text-destructive" /* ... */ />
</div>
<div>
<h2 className="text-lg font-semibold">Something went wrong</h2>
<p className="mt-1 text-sm text-muted-foreground">
{error.message || "An unexpected error occurred."}
</p>
{error.digest && (
<p className="mt-1 text-xs text-muted-foreground/60">
Error ID: {error.digest}
</p>
)}
</div>
<button
onClick={reset}
className="rounded bg-primary text-primary-foreground px-4 py-2 text-sm font-medium"
>
Try again
</button>
</div>
);
}app/global-error.tsx — catch errors in the root layout
"use client";
// Catches errors in app/layout.tsx
// Must include <html> and <body> — it replaces the root layout
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-xl font-bold">Something went critically wrong</h2>
<button onClick={reset} className="mt-4 underline text-sm">
Try to recover
</button>
</div>
</div>
</body>
</html>
);
}File structure for granular error boundaries
app/
├── global-error.tsx ← catches root layout errors
├── error.tsx ← catches root page errors
├── dashboard/
│ ├── error.tsx ← catches dashboard only
│ ├── page.tsx
│ └── analytics/
│ ├── error.tsx ← catches analytics route only
│ └── page.tsx
└── settings/
├── error.tsx ← independent from dashboard errors
└── page.tsxEach error.tsx only catches errors in the same directory and its descendants — not siblings. A crash in analytics/ shows analytics/error.tsx, not the root dashboard/error.tsx.
Decision guide
Error Boundary: which approach to use?
React class component
Use when: You need granular component-level error isolation in a non-Next.js React app. Or when you want full control without a library.
Pros: No dependencies. Full control over lifecycle.
Cons: More boilerplate. No hook for async errors.
react-error-boundary
Use when: You want the ErrorBoundary class written for you, plus useErrorBoundary() for catching async/event errors. Best for React apps and Vite/CRA projects.
Pros: useErrorBoundary() for async. Clean API. Actively maintained by React team member.
Cons: Extra dependency (small: ~1KB).
Next.js error.tsx
Use when: You're building a Next.js App Router project. For page-level error handling, error.tsx is the zero-config solution. No library needed.
Pros: No extra code or dependencies. Built into the framework. Works with React Suspense and streaming.
Cons: Route-segment level only — can't wrap individual widgets in a page.
Recommended for Next.js SaaS in 2026
Use error.tsx files for route-level errors (crashes in page.tsx, layouts, and data fetching). Use react-error-boundary's ErrorBoundary for component-level isolation within pages (e.g., wrapping individual widgets, charts, or third-party components). Log errors in componentDidCatch / onError to Sentry or PostHog.
Error logging
Logging Error Boundary catches to Sentry
Error Boundaries are only useful if you know when they trigger. Wire them to an error tracker.
Using componentDidCatch with Sentry
import * as Sentry from "@sentry/nextjs";
import { ErrorBoundary } from "react-error-boundary";
// react-error-boundary: pass onError prop
<ErrorBoundary
onError={(error, info) => {
Sentry.captureException(error, {
extra: { componentStack: info.componentStack },
});
}}
FallbackComponent={ErrorFallback}
>
<App />
</ErrorBoundary>
// Or use Sentry's own Sentry.ErrorBoundary:
import { ErrorBoundary as SentryErrorBoundary } from "@sentry/react";
<SentryErrorBoundary fallback={<p>An error occurred</p>} showDialog>
<App />
</SentryErrorBoundary>In Next.js error.tsx with Sentry
"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Sentry captures with the error.digest as the error ID
Sentry.captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}The error.digest field is a short hash identifying the underlying server error. Useful for correlating client-side error.tsx renders with server-side logs without leaking production error details to the UI.
FAQ
React Error Boundary FAQ
What is a React Error Boundary?
An Error Boundary is a React class component that catches JavaScript rendering errors in its child tree and shows a fallback UI instead of crashing the whole app. It uses getDerivedStateFromError (to update state during render) and componentDidCatch (for side effects like logging).
Can you use hooks with React Error Boundaries?
Error Boundaries themselves must be class components — there are no hook equivalents for getDerivedStateFromError and componentDidCatch. However, the react-error-boundary library provides a useErrorBoundary() hook that lets you throw errors from async code and event handlers into the nearest class-based Error Boundary.
How do I add an Error Boundary in Next.js App Router?
Add an error.tsx file to any route segment. It must be a Client Component ('use client'). It receives error and reset props. Next.js wraps the segment's page.tsx in a React Error Boundary automatically. For root-level errors, use app/global-error.tsx — which must include <html> and <body> tags.
Does an Error Boundary catch async errors?
No — not natively. Error Boundaries only catch synchronous rendering errors. For async errors (fetch, setTimeout, Promise rejections), use try/catch and either pass the error to state (which re-throws during render) or use react-error-boundary's useErrorBoundary() hook to manually trigger the boundary.
What's the difference between error.tsx and not-found.tsx in Next.js?
error.tsx catches JavaScript runtime errors (thrown exceptions). not-found.tsx displays when notFound() is called or a route doesn't exist (404). They're complementary — error.tsx for crashes, not-found.tsx for missing resources.
Related guides
More Next.js dev references
Ready to ship
Build on a production-ready Next.js foundation
ShipAI.today ships with error.tsx files pre-configured per route segment, Sentry error tracking wired in, and all the rendering edge cases handled. Skip the setup.