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.