Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { Component, type ErrorInfo, type ReactNode } from "react";
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<RiskyComponent />
</ErrorBoundary>When to reach for this: Wrap any component subtree that might throw during rendering. Essential around data-fetching components, third-party widgets, user-generated content renderers, and route-level layouts.
import {
Component,
useState,
useCallback,
type ErrorInfo,
type ReactNode,
} from "react";
// --- Full-featured error boundary with recovery ---
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
renderFallback?: (props: {
error: Error;
reset: () => void;
}) => ReactNode;
resetKeys?: unknown[];
}
interface ErrorBoundaryState {
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
if (this.state.error && prevProps.resetKeys !== this.props.resetKeys) {
// If resetKeys changed, attempt recovery
const changed = this.props.resetKeys?.some(
(key, i) => key !== prevProps.resetKeys?.[i]
);
if (changed) this.reset();
}
}
reset = () => {
this.setState({ error: null });
};
render() {
const { error } = this.state;
const { children, fallback, renderFallback } = this.props;
if (error) {
if (renderFallback) {
return renderFallback({ error, reset: this.reset });
}
return fallback ?? <p>Something went wrong.</p>;
}
return children;
}
}
// --- Usage with recovery UI ---
function UserProfile({ userId }: { userId: string }) {
return (
<ErrorBoundary
resetKeys={[userId]}
onError={(error) => {
// Report to error tracking service
reportToSentry(error);
}}
renderFallback={({ error, reset }) => (
<div className="p-6 bg-red-50 rounded-lg text-center">
<h3 className="text-red-800 font-semibold">Failed to load profile</h3>
<p className="text-red-600 mt-2">{error.message}</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Try Again
</button>
</div>
)}
>
<ProfileContent userId={userId} />
</ErrorBoundary>
);
}
// --- Granular error boundaries per section ---
function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<ErrorBoundary fallback={<WidgetError name="Revenue" />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Users" />}>
<UserStats />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Activity" />}>
<ActivityFeed />
</ErrorBoundary>
</div>
);
}
function WidgetError({ name }: { name: string }) {
return (
<div className="p-4 border border-red-200 rounded bg-red-50 text-red-700">
{name} widget failed to load.
</div>
);
}What this demonstrates:
resetKeys prop that auto-resets the boundary when dependencies change (e.g., route params)onError) for Sentry or similar servicesstatic getDerivedStateFromError() or componentDidCatch().getDerivedStateFromError runs during render to set error state and trigger fallback UI.componentDidCatch runs after commit for side effects like error logging.| Lifecycle Method | When Called | Purpose |
|---|---|---|
getDerivedStateFromError(error) | During render | Return new state to trigger fallback UI |
componentDidCatch(error, info) | After commit | Side effects: logging, error reporting |
info.componentStack | In componentDidCatch | String showing the component tree path |
Hook-based error boundary wrapper - use a package or thin wrapper for hook ergonomics:
// With react-error-boundary (popular library)
import { ErrorBoundary } from "react-error-boundary";
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => queryClient.invalidateQueries()}
resetKeys={[routeKey]}
>
<AppContent />
</ErrorBoundary>
);
}
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}Nested boundaries with different granularity:
// Page-level boundary catches everything
<ErrorBoundary fallback={<FullPageError />}>
<Layout>
{/* Section-level boundaries isolate failures */}
<ErrorBoundary fallback={<SectionError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError />}>
<MainContent />
</ErrorBoundary>
</Layout>
</ErrorBoundary>{ error: Error | null } rather than { hasError: boolean } to preserve the error object for the fallback.ErrorInfo from React for the componentDidCatch second parameter.componentStack property on ErrorInfo is a string, not a structured object.Event handler errors are not caught - Error boundaries only catch errors during React rendering. An onClick that throws will not be caught. Fix: Use try/catch in event handlers and set local error state.
Async errors are not caught - Promises that reject inside useEffect or event handlers are not caught. Fix: Catch async errors and either set local state or re-throw during render (e.g., store in state and throw in render).
No hook-based error boundaries - React does not provide a hook equivalent of getDerivedStateFromError. Fix: Use the class component pattern or the react-error-boundary library.
Error boundary itself throwing - If the error boundary's own render method throws, it propagates to the nearest parent boundary. Fix: Keep error boundary render methods simple. Never throw in the fallback UI.
Recovery without unmount - Resetting hasError to false without remounting children may leave them in a broken state. Fix: Use a key prop on the children wrapper to force remount on reset.
| Approach | Trade-off |
|---|---|
| Error boundaries | Catches render errors; requires class component |
react-error-boundary | Hook-friendly API, resetKeys; extra dependency |
| Try/catch in event handlers | Manual; doesn't catch render errors |
| Suspense with error handling | Handles async loading errors via thrown promises |
Global window.onerror | Catches uncaught errors; no React-specific recovery |
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Next.js error.tsx with logging, retry, and full-page reload fallback
// File: app/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log to external error tracking service
console.error('Application error:', error);
// reportToSentry(error);
}, [error]);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center space-y-4">
<h2 className="text-2xl font-bold">Something went wrong</h2>
{error.digest && (
<p className="text-sm text-gray-500">Error ID: {error.digest}</p>
)}
<div className="flex gap-3 justify-center">
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try again
</button>
<button
onClick={() => window.location.href = '/'}
className="px-4 py-2 border rounded hover:bg-gray-50"
>
Go home
</button>
</div>
</div>
</div>
);
}What this demonstrates in production:
error.tsx is a Next.js App Router convention that automatically wraps the page in an error boundary. You do not need to write a class-based error boundary yourself.useEffect logs the error on mount. This is the correct place to send errors to Sentry or a similar service. Do not log in the render body because it runs during React's render phase.reset() re-renders the route segment, which retries the failed component. This works well for transient errors like network failures or race conditions.error.digest is a hashed error identifier that Next.js generates for server-side errors. It is safe to display to users (it does not expose stack traces or sensitive details). In development, the full error is shown instead.window.location.href instead of router.push() as a deliberate choice. If the error corrupted the React tree or client-side router state, router.push() might fail or render the same broken page. A full page reload via window.location.href guarantees a clean start.error.tsx must be a Client Component ('use client'). Error boundaries require client-side React to catch and recover from errors.getDerivedStateFromError or componentDidCatch.react-error-boundary library if you prefer a hook-friendly wrapper API.getDerivedStateFromError runs during the render phase to update state and trigger the fallback UI. It must be a pure function.componentDidCatch runs after the commit phase for side effects like logging errors to Sentry.<ErrorBoundary
resetKeys={[userId]}
renderFallback={({ error, reset }) => (
<div>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<ProfileContent userId={userId} />
</ErrorBoundary>resetKeys change (e.g., userId changes), the boundary automatically resets and retries rendering.onClick handler that throws does not interrupt rendering; it throws in the browser's event loop.interface ErrorBoundaryState {
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
}{ error: Error | null } to preserve the error object for the fallback.ErrorInfo from React for componentDidCatch's second parameter.error.digest is a hashed error identifier generated by Next.js for server-side errors.router.push() might fail or render the same broken page.window.location.href triggers a full page reload, guaranteeing a clean start.hasError to false re-renders the same children, which may still be broken.key prop on the children wrapper to force a full unmount and remount on reset.childrenReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥