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 itselfHook 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.