Back to Blog
ReactError HandlingError BoundariesResilience

React Error Boundaries Guide

Handle errors gracefully in React. From basic error boundaries to recovery patterns to error reporting.

B
Bootspring Team
Engineering
May 8, 2021
6 min read

Error boundaries catch JavaScript errors in components and display fallback UI. Here's how to use them effectively.

Basic Error Boundary#

1import { Component, ErrorInfo, ReactNode } from 'react'; 2 3interface Props { 4 children: ReactNode; 5 fallback?: ReactNode; 6} 7 8interface State { 9 hasError: boolean; 10 error: Error | null; 11} 12 13class ErrorBoundary extends Component<Props, State> { 14 constructor(props: Props) { 15 super(props); 16 this.state = { hasError: false, error: null }; 17 } 18 19 static getDerivedStateFromError(error: Error): State { 20 return { hasError: true, error }; 21 } 22 23 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 24 console.error('Error caught by boundary:', error, errorInfo); 25 } 26 27 render() { 28 if (this.state.hasError) { 29 return this.props.fallback || <h1>Something went wrong.</h1>; 30 } 31 32 return this.props.children; 33 } 34} 35 36// Usage 37function App() { 38 return ( 39 <ErrorBoundary fallback={<ErrorFallback />}> 40 <MyComponent /> 41 </ErrorBoundary> 42 ); 43}

Error Boundary with Reset#

1interface Props { 2 children: ReactNode; 3 fallback: (props: { error: Error; reset: () => void }) => ReactNode; 4} 5 6interface State { 7 hasError: boolean; 8 error: Error | null; 9} 10 11class ErrorBoundary extends Component<Props, State> { 12 constructor(props: Props) { 13 super(props); 14 this.state = { hasError: false, error: null }; 15 } 16 17 static getDerivedStateFromError(error: Error): State { 18 return { hasError: true, error }; 19 } 20 21 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 22 // Log to error reporting service 23 logErrorToService(error, errorInfo); 24 } 25 26 reset = () => { 27 this.setState({ hasError: false, error: null }); 28 }; 29 30 render() { 31 if (this.state.hasError && this.state.error) { 32 return this.props.fallback({ 33 error: this.state.error, 34 reset: this.reset, 35 }); 36 } 37 38 return this.props.children; 39 } 40} 41 42// Usage 43function App() { 44 return ( 45 <ErrorBoundary 46 fallback={({ error, reset }) => ( 47 <div> 48 <h2>Something went wrong</h2> 49 <p>{error.message}</p> 50 <button onClick={reset}>Try again</button> 51 </div> 52 )} 53 > 54 <MyComponent /> 55 </ErrorBoundary> 56 ); 57}

react-error-boundary Library#

1import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary'; 2 3function ErrorFallback({ error, resetErrorBoundary }) { 4 return ( 5 <div role="alert"> 6 <h2>Something went wrong</h2> 7 <pre>{error.message}</pre> 8 <button onClick={resetErrorBoundary}>Try again</button> 9 </div> 10 ); 11} 12 13function App() { 14 return ( 15 <ErrorBoundary 16 FallbackComponent={ErrorFallback} 17 onError={(error, info) => { 18 // Log error to service 19 logError(error, info); 20 }} 21 onReset={() => { 22 // Reset app state 23 queryClient.clear(); 24 }} 25 > 26 <MyApp /> 27 </ErrorBoundary> 28 ); 29} 30 31// Trigger error boundary from event handlers 32function DataLoader() { 33 const { showBoundary } = useErrorBoundary(); 34 35 async function loadData() { 36 try { 37 const data = await fetchData(); 38 setData(data); 39 } catch (error) { 40 showBoundary(error); 41 } 42 } 43 44 return <button onClick={loadData}>Load Data</button>; 45}

Granular Error Boundaries#

1// Wrap different sections independently 2function Dashboard() { 3 return ( 4 <div className="dashboard"> 5 <ErrorBoundary fallback={<HeaderError />}> 6 <Header /> 7 </ErrorBoundary> 8 9 <main> 10 <ErrorBoundary fallback={<ChartError />}> 11 <ChartSection /> 12 </ErrorBoundary> 13 14 <ErrorBoundary fallback={<TableError />}> 15 <DataTable /> 16 </ErrorBoundary> 17 </main> 18 19 <ErrorBoundary fallback={<SidebarError />}> 20 <Sidebar /> 21 </ErrorBoundary> 22 </div> 23 ); 24} 25 26// Section-specific fallbacks 27function ChartError() { 28 return ( 29 <div className="chart-error"> 30 <p>Unable to load chart</p> 31 <button onClick={() => window.location.reload()}> 32 Refresh page 33 </button> 34 </div> 35 ); 36}

Error Boundary with Retry#

1interface RetryBoundaryProps { 2 children: ReactNode; 3 maxRetries?: number; 4} 5 6interface State { 7 hasError: boolean; 8 error: Error | null; 9 retryCount: number; 10} 11 12class RetryBoundary extends Component<RetryBoundaryProps, State> { 13 constructor(props: RetryBoundaryProps) { 14 super(props); 15 this.state = { hasError: false, error: null, retryCount: 0 }; 16 } 17 18 static getDerivedStateFromError(error: Error): Partial<State> { 19 return { hasError: true, error }; 20 } 21 22 retry = () => { 23 this.setState(prev => ({ 24 hasError: false, 25 error: null, 26 retryCount: prev.retryCount + 1, 27 })); 28 }; 29 30 render() { 31 const { maxRetries = 3 } = this.props; 32 const { hasError, error, retryCount } = this.state; 33 34 if (hasError) { 35 if (retryCount >= maxRetries) { 36 return ( 37 <div> 38 <h2>Failed after {maxRetries} attempts</h2> 39 <p>{error?.message}</p> 40 </div> 41 ); 42 } 43 44 return ( 45 <div> 46 <h2>Something went wrong</h2> 47 <p>Attempt {retryCount + 1} of {maxRetries}</p> 48 <button onClick={this.retry}>Retry</button> 49 </div> 50 ); 51 } 52 53 return this.props.children; 54 } 55}

Suspense with Error Boundary#

1import { Suspense } from 'react'; 2import { ErrorBoundary } from 'react-error-boundary'; 3 4function AsyncComponent() { 5 return ( 6 <ErrorBoundary 7 fallback={<ErrorMessage />} 8 onReset={() => { 9 // Clear cache or refetch 10 }} 11 > 12 <Suspense fallback={<Loading />}> 13 <DataComponent /> 14 </Suspense> 15 </ErrorBoundary> 16 ); 17} 18 19// Combined loading and error states 20function AsyncSection() { 21 return ( 22 <ErrorBoundary fallback={<SectionError />}> 23 <Suspense fallback={<SectionSkeleton />}> 24 <AsyncContent /> 25 </Suspense> 26 </ErrorBoundary> 27 ); 28}

Error Reporting Integration#

1import * as Sentry from '@sentry/react'; 2 3// Sentry's built-in error boundary 4function App() { 5 return ( 6 <Sentry.ErrorBoundary 7 fallback={({ error, resetError }) => ( 8 <ErrorFallback error={error} onReset={resetError} /> 9 )} 10 showDialog 11 > 12 <MyApp /> 13 </Sentry.ErrorBoundary> 14 ); 15} 16 17// Custom error boundary with reporting 18class ReportingErrorBoundary extends Component<Props, State> { 19 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 20 // Sentry 21 Sentry.captureException(error, { 22 extra: { componentStack: errorInfo.componentStack }, 23 }); 24 25 // Or custom logging 26 fetch('/api/log-error', { 27 method: 'POST', 28 body: JSON.stringify({ 29 message: error.message, 30 stack: error.stack, 31 componentStack: errorInfo.componentStack, 32 url: window.location.href, 33 timestamp: new Date().toISOString(), 34 }), 35 }); 36 } 37 38 // ... rest of implementation 39}

Error Types and Handling#

1// Custom error types 2class NetworkError extends Error { 3 constructor(message: string, public statusCode?: number) { 4 super(message); 5 this.name = 'NetworkError'; 6 } 7} 8 9class ValidationError extends Error { 10 constructor(message: string, public field?: string) { 11 super(message); 12 this.name = 'ValidationError'; 13 } 14} 15 16// Type-specific fallbacks 17function ErrorFallback({ error, resetErrorBoundary }) { 18 if (error instanceof NetworkError) { 19 return ( 20 <div> 21 <h2>Network Error</h2> 22 <p>Please check your connection</p> 23 <button onClick={resetErrorBoundary}>Retry</button> 24 </div> 25 ); 26 } 27 28 if (error instanceof ValidationError) { 29 return ( 30 <div> 31 <h2>Invalid Data</h2> 32 <p>{error.message}</p> 33 <button onClick={resetErrorBoundary}>Go Back</button> 34 </div> 35 ); 36 } 37 38 return ( 39 <div> 40 <h2>Unexpected Error</h2> 41 <button onClick={() => window.location.reload()}> 42 Refresh Page 43 </button> 44 </div> 45 ); 46}

Testing Error Boundaries#

1import { render, screen } from '@testing-library/react'; 2import userEvent from '@testing-library/user-event'; 3 4// Component that throws 5function ThrowError({ shouldThrow }: { shouldThrow: boolean }) { 6 if (shouldThrow) { 7 throw new Error('Test error'); 8 } 9 return <div>No error</div>; 10} 11 12describe('ErrorBoundary', () => { 13 // Suppress console.error for expected errors 14 const originalError = console.error; 15 beforeAll(() => { 16 console.error = jest.fn(); 17 }); 18 afterAll(() => { 19 console.error = originalError; 20 }); 21 22 it('renders children when no error', () => { 23 render( 24 <ErrorBoundary fallback={<div>Error</div>}> 25 <ThrowError shouldThrow={false} /> 26 </ErrorBoundary> 27 ); 28 29 expect(screen.getByText('No error')).toBeInTheDocument(); 30 }); 31 32 it('renders fallback when error occurs', () => { 33 render( 34 <ErrorBoundary fallback={<div>Error occurred</div>}> 35 <ThrowError shouldThrow={true} /> 36 </ErrorBoundary> 37 ); 38 39 expect(screen.getByText('Error occurred')).toBeInTheDocument(); 40 }); 41 42 it('resets error state', async () => { 43 const user = userEvent.setup(); 44 let shouldThrow = true; 45 46 function Fallback({ resetErrorBoundary }) { 47 return ( 48 <button onClick={() => { 49 shouldThrow = false; 50 resetErrorBoundary(); 51 }}> 52 Reset 53 </button> 54 ); 55 } 56 57 render( 58 <ErrorBoundary FallbackComponent={Fallback}> 59 <ThrowError shouldThrow={shouldThrow} /> 60 </ErrorBoundary> 61 ); 62 63 await user.click(screen.getByText('Reset')); 64 expect(screen.getByText('No error')).toBeInTheDocument(); 65 }); 66});

Best Practices#

Placement: ✓ Wrap major sections independently ✓ Place near data fetching components ✓ Keep user-interactive areas recoverable ✓ Don't wrap everything in one boundary Recovery: ✓ Provide clear error messages ✓ Offer retry or reset options ✓ Clear relevant cache on reset ✓ Log errors for debugging UX: ✓ Match fallback UI to component style ✓ Preserve surrounding layout ✓ Show actionable recovery steps ✓ Consider automatic retry for network errors

Conclusion#

Error boundaries prevent entire app crashes from component errors. Place them strategically around major sections, provide clear recovery options, and integrate with error reporting services. Use the react-error-boundary library for hooks support and advanced features.

Share this article

Help spread the word about Bootspring