Error Boundary Patterns
React error boundary hierarchy and graceful degradation patterns.
Error Boundary Component
// Class-based (React requirement for componentDidCatch)
interface ErrorBoundaryProps {
children: React.ReactNode
fallback?: React.ComponentType<FallbackProps>
onError?: (error: Error, info: React.ErrorInfo) => void
}
interface ErrorBoundaryState {
error: Error | null
}
export interface FallbackProps {
error: Error
reset: () => void
}
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('[ErrorBoundary]', error, info.componentStack)
this.props.onError?.(error, info)
}
reset = () => this.setState({ error: null })
render() {
if (this.state.error) {
const Fallback = this.props.fallback ?? DefaultFallback
return <Fallback error={this.state.error} reset={this.reset} />
}
return this.props.children
}
}
// react-error-boundary library (recommended — battle-tested)
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'
<ErrorBoundary
FallbackComponent={MyFallback}
onError={(error, info) => reportToSentry(error, info)}
onReset={() => queryClient.clear()}
>
<App />
</ErrorBoundary>
Error Boundary Hierarchy
App (top-level — catches everything, shows full-page error)
├── Navigation (no boundary — nav errors bubble to app)
├── <ErrorBoundary fallback={PageError}> ← page level
│ └── DashboardPage
│ ├── <ErrorBoundary fallback={SectionError}> ← section level
│ │ └── StatsSection
│ └── <ErrorBoundary fallback={WidgetError}> ← widget level
│ └── ChartWidget (one fails, others work)
└── Sidebar (separate boundary — sidebar error ≠ main content error)
// Page-level boundary (reset on navigation)
import { usePathname } from 'next/navigation'
import { ErrorBoundary } from 'react-error-boundary'
export function PageErrorBoundary({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<ErrorBoundary
key={pathname} // reset boundary on route change
FallbackComponent={PageErrorFallback}
onError={reportError}
>
{children}
</ErrorBoundary>
)
}
// Widget-level boundary (isolated, inline fallback)
export function WidgetErrorBoundary({ children, name }: { children: React.ReactNode; name: string }) {
return (
<ErrorBoundary
fallback={<WidgetErrorFallback name={name} />}
onError={(err) => reportError(err, { widget: name })}
>
{children}
</ErrorBoundary>
)
}
Fallback UI Design Patterns
// Full-page fallback (app boundary)
function PageErrorFallback({ error, reset }: FallbackProps) {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-8 text-center">
<h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
<p className="max-w-md text-gray-500">
{process.env.NODE_ENV === 'development'
? error.message
: 'An unexpected error occurred. Our team has been notified.'}
</p>
<div className="flex gap-3">
<button onClick={reset} className="btn btn-primary">Try again</button>
<a href="/" className="btn btn-outline">Go home</a>
</div>
{process.env.NODE_ENV === 'development' && (
<pre className="mt-4 max-w-2xl overflow-auto rounded bg-red-50 p-4 text-left text-sm text-red-800">
{error.stack}
</pre>
)}
</div>
)
}
// Inline section fallback (section boundary)
function SectionErrorFallback({ error, reset }: FallbackProps) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
<p className="text-red-700 font-medium">Failed to load this section</p>
<button onClick={reset} className="mt-3 text-sm text-red-600 underline hover:no-underline">
Retry
</button>
</div>
)
}
// Toast-style fallback (non-critical widget)
function WidgetErrorFallback({ name }: { name: string }) {
return (
<div className="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700">
<span>⚠</span>
<span>{name} unavailable</span>
</div>
)
}
Retry Mechanism in Error Boundaries
import { ErrorBoundary } from 'react-error-boundary'
import { useState, useCallback } from 'react'
function RetryFallback({ error, reset }: FallbackProps) {
const [retries, setRetries] = useState(0)
const maxRetries = 3
const handleRetry = useCallback(() => {
if (retries < maxRetries) {
setRetries(r => r + 1)
reset()
}
}, [retries, reset])
return (
<div className="rounded-lg border bg-white p-6 text-center">
<p className="font-medium text-gray-900">Failed to load</p>
{retries < maxRetries ? (
<button onClick={handleRetry} className="mt-4 btn btn-sm">
Retry ({maxRetries - retries} left)
</button>
) : (
<p className="mt-4 text-sm text-gray-400">
Please refresh the page or <a href="/support" className="underline">contact support</a>.
</p>
)}
</div>
)
}
Error Reporting (Sentry Integration)
import * as Sentry from '@sentry/nextjs'
export function reportError(
error: Error,
context?: Record<string, unknown>,
info?: React.ErrorInfo
) {
if (process.env.NODE_ENV === 'development') {
console.error('[Error]', error, context)
return
}
Sentry.withScope(scope => {
if (context) scope.setExtras(context)
if (info?.componentStack) scope.setExtra('componentStack', info.componentStack)
Sentry.captureException(error)
})
}
Offline Detection and Fallback UI
'use client'
import { useState, useEffect } from 'react'
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(
typeof window !== 'undefined' ? navigator.onLine : true
)
useEffect(() => {
const on = () => setIsOnline(true)
const off = () => setIsOnline(false)
window.addEventListener('online', on)
window.addEventListener('offline', off)
return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off) }
}, [])
return isOnline
}
export function OfflineBanner() {
const isOnline = useOnlineStatus()
if (isOnline) return null
return (
<div role="status" className="fixed top-0 inset-x-0 z-50 bg-amber-500 py-2 text-center text-sm font-medium text-white">
You are offline. Changes will sync when connection is restored.
</div>
)
}
Partial Failure Handling
// Multiple independent widgets — one fails, others keep working
export function Dashboard() {
return (
<div className="grid grid-cols-2 gap-4">
<WidgetErrorBoundary name="Revenue Chart"><RevenueChart /></WidgetErrorBoundary>
<WidgetErrorBoundary name="User Stats"><UserStats /></WidgetErrorBoundary>
<WidgetErrorBoundary name="Activity Feed"><ActivityFeed /></WidgetErrorBoundary>
<WidgetErrorBoundary name="Notifications"><NotificationPanel /></WidgetErrorBoundary>
</div>
)
}
// Graceful degradation: show stale data when fetch fails
async function StatsWidget() {
try {
const stats = await fetchStats()
return <StatsDisplay stats={stats} fresh />
} catch {
const cached = await getCachedStats()
if (cached) return <StatsDisplay stats={cached} stale />
throw new Error('Stats unavailable') // let error boundary handle
}
}
Error Recovery (Reset on Navigation)
import { usePathname } from 'next/navigation'
import { useEffect,