Back to Blog
ReactError HandlingComponentsDebugging

React Error Boundaries Guide

Master React error boundaries. From basics to recovery patterns to error reporting.

B
Bootspring Team
Engineering
August 29, 2020
7 min read

Error boundaries catch JavaScript errors in component trees. 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 // Update state to show fallback UI 21 return { hasError: true, error }; 22 } 23 24 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 // Log error to reporting service 26 console.error('Error caught:', error, errorInfo); 27 } 28 29 render() { 30 if (this.state.hasError) { 31 return this.props.fallback || <h1>Something went wrong.</h1>; 32 } 33 34 return this.props.children; 35 } 36} 37 38// Usage 39function App() { 40 return ( 41 <ErrorBoundary fallback={<ErrorFallback />}> 42 <MyComponent /> 43 </ErrorBoundary> 44 ); 45}

Error Boundary with Reset#

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

Reset on Props Change#

1interface Props { 2 children: ReactNode; 3 fallback: ReactNode; 4 resetKeys?: unknown[]; 5} 6 7interface State { 8 hasError: boolean; 9 error: Error | null; 10} 11 12class ErrorBoundary extends Component<Props, State> { 13 state: State = { hasError: false, error: null }; 14 15 static getDerivedStateFromError(error: Error): State { 16 return { hasError: true, error }; 17 } 18 19 static getDerivedStateFromProps(props: Props, state: State): State | null { 20 // Reset error when resetKeys change 21 if (state.hasError) { 22 return { hasError: false, error: null }; 23 } 24 return null; 25 } 26 27 componentDidUpdate(prevProps: Props) { 28 if (this.state.hasError && this.props.resetKeys !== prevProps.resetKeys) { 29 this.setState({ hasError: false, error: null }); 30 } 31 } 32 33 render() { 34 if (this.state.hasError) { 35 return this.props.fallback; 36 } 37 return this.props.children; 38 } 39} 40 41// Usage - resets when userId changes 42function UserProfile({ userId }: { userId: string }) { 43 return ( 44 <ErrorBoundary resetKeys={[userId]} fallback={<UserError />}> 45 <UserData userId={userId} /> 46 </ErrorBoundary> 47 ); 48}

Multiple Boundaries#

1function App() { 2 return ( 3 <div className="app"> 4 {/* Top-level catch-all */} 5 <ErrorBoundary fallback={<AppError />}> 6 <Header /> 7 8 {/* Isolated sections */} 9 <main> 10 <ErrorBoundary fallback={<SidebarError />}> 11 <Sidebar /> 12 </ErrorBoundary> 13 14 <ErrorBoundary fallback={<ContentError />}> 15 <Content /> 16 </ErrorBoundary> 17 </main> 18 19 <ErrorBoundary fallback={<FooterError />}> 20 <Footer /> 21 </ErrorBoundary> 22 </ErrorBoundary> 23 </div> 24 ); 25} 26 27// Widget with isolated errors 28function Dashboard() { 29 return ( 30 <div className="dashboard"> 31 {widgets.map(widget => ( 32 <ErrorBoundary 33 key={widget.id} 34 fallback={<WidgetError widget={widget} />} 35 > 36 <Widget {...widget} /> 37 </ErrorBoundary> 38 ))} 39 </div> 40 ); 41}

Error Reporting#

1import * as Sentry from '@sentry/react'; 2 3class ErrorBoundary extends Component<Props, State> { 4 componentDidCatch(error: Error, errorInfo: ErrorInfo) { 5 // Report to Sentry 6 Sentry.withScope(scope => { 7 scope.setExtras({ 8 componentStack: errorInfo.componentStack, 9 }); 10 Sentry.captureException(error); 11 }); 12 13 // Or custom reporting 14 reportError({ 15 error: { 16 message: error.message, 17 stack: error.stack, 18 }, 19 componentStack: errorInfo.componentStack, 20 timestamp: new Date().toISOString(), 21 url: window.location.href, 22 userAgent: navigator.userAgent, 23 }); 24 } 25} 26 27// Using Sentry's built-in ErrorBoundary 28import { ErrorBoundary } from '@sentry/react'; 29 30function App() { 31 return ( 32 <ErrorBoundary 33 fallback={({ error, resetError }) => ( 34 <ErrorFallback error={error} onReset={resetError} /> 35 )} 36 onError={(error, componentStack) => { 37 console.error('Sentry error:', error, componentStack); 38 }} 39 > 40 <MyApp /> 41 </ErrorBoundary> 42 ); 43}

Error Fallback Components#

1// Simple fallback 2function ErrorFallback({ error }: { error: Error }) { 3 return ( 4 <div role="alert" className="error-fallback"> 5 <h2>Something went wrong</h2> 6 <pre>{error.message}</pre> 7 </div> 8 ); 9} 10 11// Fallback with retry 12function RetryFallback({ 13 error, 14 resetErrorBoundary, 15}: { 16 error: Error; 17 resetErrorBoundary: () => void; 18}) { 19 return ( 20 <div role="alert" className="error-fallback"> 21 <h2>Oops! Something went wrong</h2> 22 <p>{error.message}</p> 23 <button onClick={resetErrorBoundary}>Try again</button> 24 </div> 25 ); 26} 27 28// Development vs production fallback 29function ErrorFallback({ error }: { error: Error }) { 30 const isDev = process.env.NODE_ENV === 'development'; 31 32 return ( 33 <div role="alert" className="error-fallback"> 34 <h2>Something went wrong</h2> 35 {isDev ? ( 36 <pre className="error-stack">{error.stack}</pre> 37 ) : ( 38 <p>Please try refreshing the page.</p> 39 )} 40 </div> 41 ); 42} 43 44// Full-page error 45function FullPageError({ error, reset }: ErrorFallbackProps) { 46 return ( 47 <div className="full-page-error"> 48 <div className="error-content"> 49 <h1>We hit a snag</h1> 50 <p>Something unexpected happened. Our team has been notified.</p> 51 <div className="error-actions"> 52 <button onClick={reset}>Try Again</button> 53 <button onClick={() => window.location.reload()}> 54 Refresh Page 55 </button> 56 <a href="/">Go Home</a> 57 </div> 58 {process.env.NODE_ENV === 'development' && ( 59 <details> 60 <summary>Error Details</summary> 61 <pre>{error.stack}</pre> 62 </details> 63 )} 64 </div> 65 </div> 66 ); 67}

What Errors Are Caught#

1// CAUGHT: Errors in render 2function BrokenComponent() { 3 throw new Error('Render error'); 4 return <div>Never rendered</div>; 5} 6 7// CAUGHT: Errors in lifecycle methods 8class BrokenLifecycle extends Component { 9 componentDidMount() { 10 throw new Error('Lifecycle error'); 11 } 12} 13 14// CAUGHT: Errors in constructors 15class BrokenConstructor extends Component { 16 constructor(props) { 17 super(props); 18 throw new Error('Constructor error'); 19 } 20} 21 22// NOT CAUGHT: Event handlers 23function ComponentWithHandler() { 24 const handleClick = () => { 25 throw new Error('Click error'); // NOT caught by boundary 26 }; 27 return <button onClick={handleClick}>Click</button>; 28} 29 30// Solution: Handle event errors manually 31function ComponentWithSafeHandler() { 32 const [error, setError] = useState<Error | null>(null); 33 34 const handleClick = () => { 35 try { 36 doSomethingRisky(); 37 } catch (err) { 38 setError(err as Error); 39 } 40 }; 41 42 if (error) { 43 return <ErrorDisplay error={error} />; 44 } 45 46 return <button onClick={handleClick}>Click</button>; 47} 48 49// NOT CAUGHT: Async code 50function AsyncComponent() { 51 useEffect(() => { 52 fetchData().catch(error => { 53 // Handle async errors manually 54 }); 55 }, []); 56} 57 58// NOT CAUGHT: Server-side rendering errors 59// NOT CAUGHT: Errors in error boundary itself

Hook for Error Handling#

1// Custom hook for error state 2function useErrorHandler() { 3 const [error, setError] = useState<Error | null>(null); 4 5 const handleError = useCallback((error: Error) => { 6 setError(error); 7 }, []); 8 9 const resetError = useCallback(() => { 10 setError(null); 11 }, []); 12 13 // Throw to nearest error boundary 14 if (error) { 15 throw error; 16 } 17 18 return { handleError, resetError }; 19} 20 21// Usage 22function MyComponent() { 23 const { handleError } = useErrorHandler(); 24 25 const handleClick = async () => { 26 try { 27 await riskyOperation(); 28 } catch (error) { 29 handleError(error as Error); 30 } 31 }; 32 33 return <button onClick={handleClick}>Do Something</button>; 34}

react-error-boundary Library#

1import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary'; 2 3function ErrorFallback({ error, resetErrorBoundary }) { 4 return ( 5 <div role="alert"> 6 <p>Something went wrong:</p> 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 onReset={() => { 18 // Reset app state 19 }} 20 resetKeys={[someKey]} 21 > 22 <MyComponent /> 23 </ErrorBoundary> 24 ); 25} 26 27// Hook to throw to boundary 28function ChildComponent() { 29 const { showBoundary } = useErrorBoundary(); 30 31 const handleClick = async () => { 32 try { 33 await fetchData(); 34 } catch (error) { 35 showBoundary(error); 36 } 37 }; 38 39 return <button onClick={handleClick}>Load</button>; 40}

Best Practices#

Placement: ✓ Wrap route components ✓ Isolate independent widgets ✓ Keep boundaries granular ✓ Have a top-level catch-all Recovery: ✓ Provide reset functionality ✓ Reset on key prop changes ✓ Clear error state properly ✓ Show helpful messages Reporting: ✓ Log to error service ✓ Include component stack ✓ Add user context ✓ Track error frequency Avoid: ✗ Catching everything silently ✗ Showing technical errors to users ✗ Error boundaries in event handlers ✗ Forgetting async error handling

Conclusion#

Error boundaries prevent crashes from taking down entire apps. Use multiple boundaries to isolate failures, provide reset functionality for recovery, and report errors for debugging. Remember they only catch render errors—handle event and async errors separately.

Share this article

Help spread the word about Bootspring