Error boundaries prevent entire app crashes from component errors. Here's how to implement them effectively.
Basic Error Boundary#
1import { Component, ErrorInfo, ReactNode } from 'react';
2
3interface ErrorBoundaryProps {
4 children: ReactNode;
5 fallback?: ReactNode;
6 onError?: (error: Error, errorInfo: ErrorInfo) => void;
7}
8
9interface ErrorBoundaryState {
10 hasError: boolean;
11 error: Error | null;
12}
13
14class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
15 constructor(props: ErrorBoundaryProps) {
16 super(props);
17 this.state = { hasError: false, error: null };
18 }
19
20 static getDerivedStateFromError(error: Error): ErrorBoundaryState {
21 // Update state so next render shows fallback UI
22 return { hasError: true, error };
23 }
24
25 componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26 // Log error to reporting service
27 console.error('Error caught by boundary:', error, errorInfo);
28 this.props.onError?.(error, errorInfo);
29 }
30
31 render() {
32 if (this.state.hasError) {
33 return this.props.fallback || <DefaultErrorFallback error={this.state.error} />;
34 }
35
36 return this.props.children;
37 }
38}
39
40function DefaultErrorFallback({ error }: { error: Error | null }) {
41 return (
42 <div role="alert" className="error-fallback">
43 <h2>Something went wrong</h2>
44 <p>{error?.message}</p>
45 <button onClick={() => window.location.reload()}>
46 Reload page
47 </button>
48 </div>
49 );
50}
51
52// Usage
53function App() {
54 return (
55 <ErrorBoundary
56 fallback={<p>Something went wrong</p>}
57 onError={(error, info) => logToService(error, info)}
58 >
59 <MainContent />
60 </ErrorBoundary>
61 );
62}Resettable Error Boundary#
1import { Component, ErrorInfo, ReactNode } from 'react';
2
3interface Props {
4 children: ReactNode;
5 fallbackRender: (props: { error: Error; reset: () => void }) => ReactNode;
6 onReset?: () => void;
7 resetKeys?: unknown[];
8}
9
10interface State {
11 error: Error | null;
12}
13
14class ResettableErrorBoundary extends Component<Props, State> {
15 state: State = { error: null };
16
17 static getDerivedStateFromError(error: Error): State {
18 return { error };
19 }
20
21 componentDidUpdate(prevProps: Props) {
22 // Reset when resetKeys change
23 if (this.state.error && prevProps.resetKeys !== this.props.resetKeys) {
24 this.reset();
25 }
26 }
27
28 reset = () => {
29 this.props.onReset?.();
30 this.setState({ error: null });
31 };
32
33 render() {
34 if (this.state.error) {
35 return this.props.fallbackRender({
36 error: this.state.error,
37 reset: this.reset,
38 });
39 }
40
41 return this.props.children;
42 }
43}
44
45// Usage with reset
46function UserProfile({ userId }: { userId: string }) {
47 return (
48 <ResettableErrorBoundary
49 resetKeys={[userId]} // Reset when userId changes
50 onReset={() => console.log('Error boundary reset')}
51 fallbackRender={({ error, reset }) => (
52 <div>
53 <p>Failed to load user: {error.message}</p>
54 <button onClick={reset}>Try again</button>
55 </div>
56 )}
57 >
58 <UserData userId={userId} />
59 </ResettableErrorBoundary>
60 );
61}react-error-boundary Library#
1import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
2
3// Declarative usage
4function App() {
5 return (
6 <ErrorBoundary
7 FallbackComponent={ErrorFallback}
8 onError={(error, info) => logErrorToService(error, info)}
9 onReset={(details) => {
10 // Reset app state here
11 console.log('Reset with details:', details);
12 }}
13 resetKeys={[someStateValue]}
14 >
15 <AppContent />
16 </ErrorBoundary>
17 );
18}
19
20// Fallback component
21function ErrorFallback({
22 error,
23 resetErrorBoundary,
24}: {
25 error: Error;
26 resetErrorBoundary: () => void;
27}) {
28 return (
29 <div role="alert">
30 <h2>Something went wrong:</h2>
31 <pre>{error.message}</pre>
32 <button onClick={resetErrorBoundary}>Try again</button>
33 </div>
34 );
35}
36
37// Hook for programmatic error throwing
38function DataLoader() {
39 const { showBoundary } = useErrorBoundary();
40
41 useEffect(() => {
42 fetchData()
43 .then(setData)
44 .catch((error) => {
45 showBoundary(error); // Trigger error boundary
46 });
47 }, [showBoundary]);
48
49 return <div>Loading...</div>;
50}
51
52// Inline fallback
53function SimpleComponent() {
54 return (
55 <ErrorBoundary fallback={<p>Error loading widget</p>}>
56 <Widget />
57 </ErrorBoundary>
58 );
59}Strategic Boundary Placement#
1// App-level boundary - catches everything
2function App() {
3 return (
4 <ErrorBoundary FallbackComponent={AppErrorFallback}>
5 <Router>
6 <Layout>
7 <Routes>
8 <Route path="/" element={<Home />} />
9 <Route path="/dashboard" element={<Dashboard />} />
10 </Routes>
11 </Layout>
12 </Router>
13 </ErrorBoundary>
14 );
15}
16
17// Page-level boundaries - isolate page errors
18function Dashboard() {
19 return (
20 <ErrorBoundary FallbackComponent={PageErrorFallback}>
21 <DashboardHeader />
22 <DashboardContent />
23 <DashboardFooter />
24 </ErrorBoundary>
25 );
26}
27
28// Component-level boundaries - isolate widget errors
29function DashboardContent() {
30 return (
31 <div className="grid">
32 <ErrorBoundary fallback={<WidgetError />}>
33 <StatsWidget />
34 </ErrorBoundary>
35
36 <ErrorBoundary fallback={<WidgetError />}>
37 <ChartWidget />
38 </ErrorBoundary>
39
40 <ErrorBoundary fallback={<WidgetError />}>
41 <ActivityWidget />
42 </ErrorBoundary>
43 </div>
44 );
45}
46
47// Reusable wrapper
48function WidgetErrorBoundary({ children }: { children: ReactNode }) {
49 return (
50 <ErrorBoundary
51 fallback={
52 <div className="widget-error">
53 <p>Failed to load widget</p>
54 <button onClick={() => window.location.reload()}>
55 Refresh
56 </button>
57 </div>
58 }
59 >
60 {children}
61 </ErrorBoundary>
62 );
63}Error Logging#
1// Error logging service integration
2interface ErrorReport {
3 error: Error;
4 errorInfo: ErrorInfo;
5 componentStack: string;
6 timestamp: Date;
7 url: string;
8 userAgent: string;
9 userId?: string;
10}
11
12async function logError(error: Error, errorInfo: ErrorInfo) {
13 const report: ErrorReport = {
14 error: {
15 name: error.name,
16 message: error.message,
17 stack: error.stack,
18 } as Error,
19 errorInfo,
20 componentStack: errorInfo.componentStack,
21 timestamp: new Date(),
22 url: window.location.href,
23 userAgent: navigator.userAgent,
24 userId: getCurrentUserId(),
25 };
26
27 try {
28 await fetch('/api/errors', {
29 method: 'POST',
30 headers: { 'Content-Type': 'application/json' },
31 body: JSON.stringify(report),
32 });
33 } catch (e) {
34 console.error('Failed to log error:', e);
35 }
36}
37
38// With Sentry
39import * as Sentry from '@sentry/react';
40
41function App() {
42 return (
43 <Sentry.ErrorBoundary
44 fallback={({ error, resetError }) => (
45 <ErrorFallback error={error} onReset={resetError} />
46 )}
47 showDialog
48 >
49 <AppContent />
50 </Sentry.ErrorBoundary>
51 );
52}Async Error Handling#
1// Error boundaries don't catch async errors
2// Use try-catch and state for async
3
4function AsyncComponent() {
5 const [error, setError] = useState<Error | null>(null);
6 const [data, setData] = useState(null);
7
8 useEffect(() => {
9 async function loadData() {
10 try {
11 const result = await fetchData();
12 setData(result);
13 } catch (err) {
14 setError(err as Error);
15 }
16 }
17 loadData();
18 }, []);
19
20 if (error) {
21 throw error; // Re-throw to be caught by error boundary
22 }
23
24 return <DataDisplay data={data} />;
25}
26
27// Or use useErrorBoundary hook
28function AsyncWithHook() {
29 const { showBoundary } = useErrorBoundary();
30
31 useEffect(() => {
32 fetchData().catch(showBoundary);
33 }, [showBoundary]);
34
35 return <div>Content</div>;
36}
37
38// With React Query - automatic error handling
39function QueryComponent() {
40 const { data, error, isError } = useQuery({
41 queryKey: ['data'],
42 queryFn: fetchData,
43 useErrorBoundary: true, // Propagate to error boundary
44 });
45
46 return <DataDisplay data={data} />;
47}Development vs Production#
1// Different behavior for dev/prod
2function AppErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
3 const isDev = process.env.NODE_ENV === 'development';
4
5 return (
6 <div className="error-page">
7 <h1>Something went wrong</h1>
8
9 {isDev ? (
10 <>
11 <pre className="error-stack">{error.message}</pre>
12 <pre className="error-stack">{error.stack}</pre>
13 </>
14 ) : (
15 <p>We're sorry, but something unexpected happened.</p>
16 )}
17
18 <button onClick={resetErrorBoundary}>Try again</button>
19 <button onClick={() => window.location.href = '/'}>
20 Go to home page
21 </button>
22 </div>
23 );
24}
25
26// Conditional error boundary for dev
27function DevOnlyErrorDetails({ error }: { error: Error }) {
28 if (process.env.NODE_ENV !== 'development') {
29 return null;
30 }
31
32 return (
33 <details>
34 <summary>Error Details</summary>
35 <pre>{error.stack}</pre>
36 </details>
37 );
38}Best Practices#
Placement:
✓ App-level for global errors
✓ Page-level for route isolation
✓ Widget-level for independent failures
✓ Don't wrap every component
Recovery:
✓ Provide reset mechanism
✓ Use resetKeys for automatic reset
✓ Offer navigation options
✓ Log errors for debugging
UX:
✓ Show user-friendly messages
✓ Provide actionable recovery steps
✓ Maintain partial functionality
✓ Different fallbacks for severity
Testing:
✓ Test error scenarios
✓ Verify boundary catches errors
✓ Test reset functionality
✓ Test fallback rendering
Conclusion#
Error boundaries prevent cascading failures in React apps. Place them strategically at app, page, and widget levels. Use resettable boundaries for recovery, log errors for debugging, and provide helpful fallback UIs. Remember that error boundaries don't catch async errors - handle those with try-catch and state or hooks.