Error boundaries prevent your entire app from crashing when a component throws. Here is how to build robust error handling with recovery options.
Basic Error Boundary
"use client";
import { Component, type ErrorInfo, type ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => 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 };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError?.(error, errorInfo);
}
reset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
if (typeof this.props.fallback === "function") {
return this.props.fallback(this.state.error, this.reset);
}
if (this.props.fallback) {
return this.props.fallback;
}
return <DefaultErrorFallback error={this.state.error} reset={this.reset} />;
}
return this.props.children;
}
}
Default Error Fallback Component
interface ErrorFallbackProps {
error: Error;
reset: () => void;
}
function DefaultErrorFallback({ error, reset }: ErrorFallbackProps) {
return (
<div
role="alert"
className="rounded-lg border border-red-200 bg-red-50 p-6 text-center"
>
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-red-100 flex items-center justify-center">
<svg
className="h-6 w-6 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.072 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-red-800 mb-2">
Something went wrong
</h2>
<p className="text-sm text-red-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-red-600 text-white rounded-md text-sm hover:bg-red-700"
>
Try again
</button>
</div>
);
}
Next.js error.tsx for Route Segments
Next.js has built-in error boundary support with error.tsx.
// app/(site)/dashboard/error.tsx
"use client";
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function DashboardError({ error, reset }: ErrorPageProps) {
return (
<div className="flex min-h-[400px] items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold mb-2">Dashboard Error</h2>
<p className="text-muted-foreground mb-4">
{error.message || "An unexpected error occurred"}
</p>
{error.digest && (
<p className="text-xs text-muted-foreground mb-4">
Error ID: {error.digest}
</p>
)}
<div className="flex gap-2 justify-center">
<button
onClick={reset}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
Retry
</button>
<a href="/" className="px-4 py-2 border rounded-md">
Go Home
</a>
</div>
</div>
</div>
);
}
Error Boundary with Retry Logic
"use client";
import { ErrorBoundary } from "./ErrorBoundary";
import { type ReactNode, useCallback, useState } from "react";
interface RetryBoundaryProps {
children: ReactNode;
maxRetries?: number;
}
export function RetryBoundary({ children, maxRetries = 3 }: RetryBoundaryProps) {
const [retryCount, setRetryCount] = useState(0);
const [key, setKey] = useState(0);
const handleError = useCallback(() => {
// Error logging could go here
}, []);
const handleRetry = useCallback(() => {
if (retryCount < maxRetries) {
setRetryCount((prev) => prev + 1);
setKey((prev) => prev + 1); // Force remount of children
}
}, [retryCount, maxRetries]);
return (
<ErrorBoundary
key={key}
onError={handleError}
fallback={(error, reset) => (
<div role="alert" className="p-6 border rounded-lg text-center">
<p className="font-medium mb-2">Something went wrong</p>
<p className="text-sm text-muted-foreground mb-4">{error.message}</p>
{retryCount < maxRetries ? (
<button
onClick={() => {
handleRetry();
reset();
}}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm"
>
Retry ({retryCount}/{maxRetries})
</button>
) : (
<div>
<p className="text-sm text-red-500 mb-2">Maximum retries exceeded</p>
<a href="/" className="text-sm text-primary hover:underline">
Return home
</a>
</div>
)}
</div>
)}
>
{children}
</ErrorBoundary>
);
}
Granular Error Boundaries
Wrap specific sections to prevent cascade failures.
// app/(site)/dashboard/page.tsx
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
<ErrorBoundary
fallback={
<div className="p-4 border rounded-lg text-center text-muted-foreground">
Revenue chart unavailable
</div>
}
>
<Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
<RevenueChart />
</Suspense>
</ErrorBoundary>
<ErrorBoundary
fallback={
<div className="p-4 border rounded-lg text-center text-muted-foreground">
Activity feed unavailable
</div>
}
>
<Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
<ActivityFeed />
</Suspense>
</ErrorBoundary>
<ErrorBoundary>
<Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
<UserTable />
</Suspense>
</ErrorBoundary>
<ErrorBoundary>
<Suspense fallback={<div className="h-64 animate-pulse bg-muted rounded-lg" />}>
<Notifications />
</Suspense>
</ErrorBoundary>
</div>
);
}
Error Reporting Integration
// lib/error-reporting.ts
interface ErrorReport {
message: string;
stack?: string;
componentStack?: string;
url: string;
timestamp: string;
userAgent: string;
}
export function reportError(error: Error, componentStack?: string) {
const report: ErrorReport = {
message: error.message,
stack: error.stack,
componentStack,
url: typeof window !== "undefined" ? window.location.href : "",
timestamp: new Date().toISOString(),
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
};
// Send to your error tracking service
if (process.env.NODE_ENV === "production") {
fetch("/api/errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(report),
}).catch(() => {
// Silently fail — don't throw errors in error handling
});
} else {
console.error("Error report:", report);
}
}
Wire it up:
<ErrorBoundary
onError={(error, errorInfo) => {
reportError(error, errorInfo.componentStack ?? undefined);
}}
>
<App />
</ErrorBoundary>
Async Error Handling Pattern
Error boundaries only catch render errors. For async operations:
"use client";
import { useState, useCallback } from "react";
export function useAsyncAction<T>(
action: () => Promise<T>
): {
execute: () => Promise<T | undefined>;
loading: boolean;
error: Error | null;
reset: () => void;
} {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await action();
return result;
} catch (e) {
const err = e instanceof Error ? e : new Error("Unknown error");
setError(err);
} finally {
setLoading(false);
}
}, [action]);
const reset = useCallback(() => setError(null), []);
return { execute, loading, error, reset };
}
Need Resilient Web Applications?
We build production-grade applications with comprehensive error handling, monitoring, and graceful degradation. Contact us to build reliable software.