Error Boundaries
Catch and handle rendering errors with error boundaries
What Error Boundaries Catch
Error boundaries catch errors during:
- Rendering (build/commit phase)
- Lifecycle methods
- Constructors
They DO NOT catch errors in:
- Event handlers (use try/catch)
- Async code (use try/catch or catch on the promise)
- Server-side rendering
- Errors thrown in the error boundary itself
Error boundaries must be class components — React lifecycle methods
componentDidCatch and getDerivedStateFromError don't exist on function
components. React 19 may change this; for now this is the rule.
Building a Reusable ErrorBoundary
"use client"
import { Component, ReactNode, ErrorInfo } from "react"
interface Props {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
// Update state so next render shows fallback
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
// Log to error reporting service
console.error("ErrorBoundary caught:", error, info)
this.props.onError?.(error, info)
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div className="rounded border border-red-300 bg-red-50 p-4">
<h2 className="font-semibold text-red-800">Something went wrong</h2>
<p className="text-sm text-red-600">{this.state.error?.message}</p>
</div>
)
)
}
return this.props.children
}
}
Placement Strategies
Granular boundaries keep one failure from taking down the whole page.
function App() {
return (
<div>
<Header />
<ErrorBoundary fallback={<p>Sidebar crashed</p>}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Content unavailable</p>}>
<MainContent />
</ErrorBoundary>
<Footer />
</div>
)
}
Strategy: wrap each feature or independent section. One error boundary per route is too coarse — one broken widget takes the whole page.
Error Boundaries and Suspense
Combine with Suspense for robust UX:
function UserDashboard() {
return (
<ErrorBoundary fallback={<p>Failed to load profile</p>}>
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
)
}
When the data fetch or lazy load fails, Suspense triggers the error boundary above it.
HOC Pattern for Boundaries
Wrap any component with boundary protection:
function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
fallback?: ReactNode
) {
return function WithErrorBoundary(props: P) {
return (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
)
}
}
const SafeWidget = withErrorBoundary(Widget, <p>Widget unavailable</p>)
React 19 Improvements
In React 19, error handling becomes more ergonomic:
onCaughtErrorandonUncaughtErrorroot options for global error reporting- Better integration with Suspense — errors during streaming server rendering are handled gracefully
For now, class component boundaries + <Suspense> is the battle-tested pattern.
See It In Action
1import { Component, type ReactNode, type ErrorInfo } from "react"2 3class ErrorBoundary extends Component<4{ children: ReactNode; fallback?: ReactNode },5{ hasError: boolean; error?: Error }6 7> {8> state = { hasError: false }9> static getDerivedStateFromError(error: Error) {10 11 return { hasError: true, error }12 13}14componentDidCatch(error: Error, info: ErrorInfo) {15console.error("Caught:", error, info.componentStack)16}17render() {18if (this.state.hasError) {19return this.props.fallback || <h2>Something went wrong</h2>20}21return this.props.children22}23}24 25function Broken({ shouldThrow }: { shouldThrow: boolean }) {26if (shouldThrow) throw new Error("💥")27return <p>All good here</p>28}29 30// Wrap Broken inside ErrorBoundary - fallback shows instead of crash31 32<ErrorBoundary fallback={<p>⚠️ Component crashed</p>}>33<Broken shouldThrow={true} />34</ErrorBoundary>ErrorBoundary catches rendering errors via getDerivedStateFromError and shows a fallback UI. Event handler and async errors are NOT caught.