Back to Blog
ReactError HandlingComponentsUX

Error Boundaries in React Applications

Handle React errors gracefully. From error boundary components to error recovery to logging strategies.

B
Bootspring Team
Engineering
February 16, 2022
6 min read

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.

Share this article

Help spread the word about Bootspring