What are Error Boundaries and their limitations? How would you implement error handling in a React application?
Quick Answer
Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. They're essential for building robust React applications that can gracefully handle errors.
Detailed Answer
What are Error Boundaries and their limitations? How would you implement error handling in a React application?
Answer: Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. They're essential for building robust React applications that can gracefully handle errors.
What Error Boundaries Are:
Error Boundaries are class components that implement one or both of the lifecycle methods:
static getDerivedStateFromError()- Used to render a fallback UI after an error has been throwncomponentDidCatch()- Used to log error information
Basic Error Boundary Implementation:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log the error to an error reporting service
console.error('Error caught by boundary:', error, errorInfo);
// Call custom error handler if provided
this.props.onError?.(error, errorInfo);
// You can also log the error to an error reporting service
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={<div>Custom error message</div>}
onError={(error, errorInfo) => {
// Send to error reporting service
console.log('Error occurred:', error, errorInfo);
}}
>
<MyComponent />
</ErrorBoundary>
);
}
Advanced Error Boundary with Retry Functionality:
interface AdvancedErrorBoundaryProps {
children: ReactNode;
fallback?: (error: Error, retry: () => void) => ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onRetry?: () => void;
}
interface AdvancedErrorBoundaryState {
hasError: boolean;
error?: Error;
retryCount: number;
}
class AdvancedErrorBoundary extends Component<
AdvancedErrorBoundaryProps,
AdvancedErrorBoundaryState
> {
private retryTimeoutId?: NodeJS.Timeout;
constructor(props: AdvancedErrorBoundaryProps) {
super(props);
this.state = { hasError: false, retryCount: 0 };
}
static getDerivedStateFromError(error: Error): Partial<AdvancedErrorBoundaryState> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by advanced boundary:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}
componentDidUpdate(prevProps: AdvancedErrorBoundaryProps) {
// Reset error state when children change
if (prevProps.children !== this.props.children && this.state.hasError) {
this.setState({ hasError: false, error: undefined });
}
}
componentWillUnmount() {
if (this.retryTimeoutId) {
clearTimeout(this.retryTimeoutId);
}
}
handleRetry = () => {
this.setState(prevState => ({
hasError: false,
error: undefined,
retryCount: prevState.retryCount + 1
}));
this.props.onRetry?.();
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback(this.state.error!, this.handleRetry);
}
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>Error: {this.state.error?.message}</p>
<button onClick={this.handleRetry}>
Try again (Attempt {this.state.retryCount + 1})
</button>
</div>
);
}
return this.props.children;
}
}
// Usage with custom fallback
function App() {
return (
<AdvancedErrorBoundary
fallback={(error, retry) => (
<div className="custom-error">
<h2>Oops! Something went wrong</h2>
<p>{error.message}</p>
<button onClick={retry}>Retry</button>
</div>
)}
onError={(error, errorInfo) => {
// Send to error reporting service
console.log('Error occurred:', error, errorInfo);
}}
>
<MyComponent />
</AdvancedErrorBoundary>
);
}
Error Boundary Limitations:
- Event Handlers: Error Boundaries don't catch errors inside event handlers.
// This error will NOT be caught by Error Boundary
function MyComponent() {
const handleClick = () => {
throw new Error('Event handler error'); // Not caught!
};
return <button onClick={handleClick}>Click me</button>;
}
// To handle event handler errors, use try-catch
function MyComponent() {
const handleClick = () => {
try {
throw new Error('Event handler error');
} catch (error) {
console.error('Event handler error:', error);
// Handle error appropriately
}
};
return <button onClick={handleClick}>Click me</button>;
}
- Asynchronous Code: Error Boundaries don't catch errors in async code.
// This error will NOT be caught by Error Boundary
function MyComponent() {
useEffect(() => {
setTimeout(() => {
throw new Error('Async error'); // Not caught!
}, 1000);
}, []);
return <div>Component</div>;
}
// To handle async errors, use try-catch
function MyComponent() {
useEffect(() => {
const handleAsyncOperation = async () => {
try {
await someAsyncOperation();
} catch (error) {
console.error('Async error:', error);
// Handle error appropriately
}
};
handleAsyncOperation();
}, []);
return <div>Component</div>;
}
-
Server-Side Rendering: Error Boundaries don't work during server-side rendering.
-
Self-Errors: Error Boundaries don't catch errors in the Error Boundary component itself.
Comprehensive Error Handling Strategy:
// 1. Global Error Boundary
class GlobalErrorBoundary extends Component {
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log to error reporting service
this.logErrorToService(error, errorInfo);
}
logErrorToService = (error: Error, errorInfo: ErrorInfo) => {
// Send to error reporting service (e.g., Sentry, LogRocket)
console.error('Global error:', error, errorInfo);
};
render() {
if (this.state.hasError) {
return (
<div className="global-error">
<h1>Something went wrong</h1>
<p>We're sorry, but something unexpected happened.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// 2. Feature-Specific Error Boundary
class FeatureErrorBoundary extends Component {
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log feature-specific error
console.error('Feature error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="feature-error">
<h3>This feature is temporarily unavailable</h3>
<p>Please try again later.</p>
</div>
);
}
return this.props.children;
}
}
// 3. Component-Level Error Handling
function MyComponent() {
const [error, setError] = useState<Error | null>(null);
const handleAsyncOperation = async () => {
try {
await someAsyncOperation();
} catch (error) {
setError(error as Error);
}
};
if (error) {
return (
<div className="component-error">
<p>Error: {error.message}</p>
<button onClick={() => setError(null)}>Dismiss</button>
</div>
);
}
return (
<div>
<button onClick={handleAsyncOperation}>
Perform Async Operation
</button>
</div>
);
}
// 4. Custom Hook for Error Handling
function useErrorHandler() {
const [error, setError] = useState<Error | null>(null);
const handleError = useCallback((error: Error) => {
setError(error);
console.error('Error handled by hook:', error);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return { error, handleError, clearError };
}
// Usage of custom hook
function ComponentWithErrorHandling() {
const { error, handleError, clearError } = useErrorHandler();
const handleClick = () => {
try {
// Some operation that might fail
throw new Error('Something went wrong');
} catch (error) {
handleError(error as Error);
}
};
if (error) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={clearError}>Clear Error</button>
</div>
);
}
return <button onClick={handleClick}>Click me</button>;
}
Error Reporting Integration:
// Error reporting service integration
class ErrorReportingService {
static logError(error: Error, errorInfo: ErrorInfo, context?: any) {
// Send to error reporting service
console.error('Error reported:', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
context
});
// Example: Send to Sentry
// Sentry.captureException(error, {
// contexts: { react: { componentStack: errorInfo.componentStack } },
// extra: context
// });
}
}
// Enhanced Error Boundary with reporting
class ReportingErrorBoundary extends Component {
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
ErrorReportingService.logError(error, errorInfo, {
component: this.constructor.name,
timestamp: new Date().toISOString()
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We've been notified about this error.</p>
</div>
);
}
return this.props.children;
}
}
Best Practices for Error Handling:
- Use Error Boundaries at Strategic Points:
function App() {
return (
<GlobalErrorBoundary>
<Header />
<MainContent />
<Footer />
</GlobalErrorBoundary>
);
}
function MainContent() {
return (
<div>
<FeatureErrorBoundary>
<UserProfile />
</FeatureErrorBoundary>
<FeatureErrorBoundary>
<DataVisualization />
</FeatureErrorBoundary>
</div>
);
}
- Handle Different Types of Errors:
// Network errors
function useNetworkErrorHandler() {
const [networkError, setNetworkError] = useState<Error | null>(null);
const handleNetworkError = useCallback((error: Error) => {
if (error.name === 'NetworkError' || error.message.includes('fetch')) {
setNetworkError(error);
}
}, []);
return { networkError, handleNetworkError };
}
// Validation errors
function useValidationErrorHandler() {
const [validationErrors, setValidationErrors] = useState<string[]>([]);
const handleValidationError = useCallback((errors: string[]) => {
setValidationErrors(errors);
}, []);
return { validationErrors, handleValidationError };
}
- Provide Meaningful Error Messages:
function ErrorMessage({ error, type }: { error: Error, type: string }) {
const getErrorMessage = (error: Error, type: string) => {
switch (type) {
case 'network':
return 'Please check your internet connection and try again.';
case 'validation':
return 'Please check your input and try again.';
case 'permission':
return 'You do not have permission to perform this action.';
default:
return 'Something went wrong. Please try again.';
}
};
return (
<div className="error-message">
<p>{getErrorMessage(error, type)}</p>
</div>
);
}
Key Takeaways:
- Error Boundaries catch JavaScript errors in component trees and display fallback UI
- They have limitations - don't catch event handler errors, async errors, or SSR errors
- Use them strategically at different levels of your component tree
- Combine with other error handling techniques for comprehensive coverage
- Integrate with error reporting services for production monitoring
- Provide meaningful error messages to users
- Test error scenarios to ensure your error handling works correctly
Error Boundaries are a crucial part of building robust React applications, but they should be combined with other error handling techniques for comprehensive coverage.