When building React applications, we often focus on the happy path - making sure everything works when users interact with our app as expected. But what happens when something goes wrong? A single unhandled error can crash your entire React application, leaving users staring at a blank screen. This is where Error Boundaries come to the rescue.
What Are Error Boundaries ?
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. Think of them as a try...catch
block for React components.
Error Boundaries catch errors during:
- Rendering
- In lifecycle methods
- In constructors of the whole tree below them
What Error Boundaries DON'T Catch
It's important to understand the limitations:
- Event handlers (use regular try...catch for these)
- Asynchronous code (e.g., setTimeout or requestAnimationFrame callbacks)
- Errors thrown during server-side rendering
- Errors thrown in the error boundary itself
Building a Basic Error Boundary
Let's start with a simple TypeScript implementation:
import React, { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
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: React.ErrorInfo) {
// Log the error to your error reporting service
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div style={{
padding: '20px',
border: '1px solid #ff6b6b',
borderRadius: '8px',
backgroundColor: '#ffe0e0',
color: '#d63031',
textAlign: 'center',
margin: '20px'
}}>
<h2>Oops! Something went wrong</h2>
<p>We apologize for the inconvenience. Please try refreshing the page.</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '8px 16px',
backgroundColor: '#d63031',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Refresh Page
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Advanced Error Boundary with Reset Functionality
For production applications, you'll want more sophisticated error handling. Here's an enhanced version:
import { Component } from "react";
type FallbackRenderArgs = {
error: Error;
reset: () => void;
}
type Props = {
children: React.ReactNode;
fallback?: React.ReactNode;
fallbackRender?: (args: FallbackRenderArgs) => React.ReactNode;
resetKeys?: unknown[];
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
onReset?: () => void;
}
type State = {
hasError: boolean;
error?: Error | null;
}
export default class ErrorBoundary extends Component<Props, State> {
state: State = {hasError: false, error: null};
private prevResetKeys: unknown[] = [];
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.(error, errorInfo);
}
componentDidUpdate(prevProps: Props) {
if (this.props.resetKeys && this.haveResetKeysChanged(prevProps.resetKeys || [], this.props.resetKeys || [])) {
this.reset();
}
}
private haveResetKeysChanged(prevKeys: unknown[], nextKeys: unknown[]) {
return prevKeys.length !== nextKeys.length ||
prevKeys.some((key, index) => key !== nextKeys[index]);
}
private reset = () => {
this.setState({ hasError: false, error: null });
this.props.onReset?.();
}
render() {
if (this.state.hasError) {
const {fallback, fallbackRender} = this.props;
if (fallbackRender) {
return fallbackRender({ error: this.state.error!, reset: this.reset });
}
if (fallback) {
return fallback;
}
return (
<div style={{
padding: '20px',
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '8px',
color: '#856404',
}}>
<h3>Something went wrong.</h3>
<p>{this.state.error?.message}</p>
<button onClick={this.reset}>Try again</button>
</div>
)
}
return this.props.children;
}
}
Creating Custom Error Types
For better error handling, create custom error classes with additional metadata:
export class MyAppError extends Error {
details?: {
code?: string;
section?: string;
payload?: unknown;
};
constructor(message: string, details?: MyAppError['details']) {
super(message);
this.name = 'MyAppError';
this.details = details;
}
}
Using Error Boundaries in Practice
Here's how to implement error boundaries in your app:
function App() {
const [count, setCount] = useState(0);
return (
<div>
{/* Basic usage with default fallback */}
<ErrorBoundary>
<SomeComponent />
</ErrorBoundary>
{/* With custom fallback */}
<ErrorBoundary
fallback={
<div>
<h3>Custom Error UI</h3>
<p>Something went wrong in this section!</p>
</div>
}
>
<AnotherComponent />
</ErrorBoundary>
{/* With reset functionality */}
<ErrorBoundary
resetKeys={[count]}
onError={(error) => console.log('Logged to monitoring service:', error)}
onReset={() => console.log('Boundary reset')}
fallbackRender={({ error, reset }) => (
<div>
<h3>Error: {error.name}</h3>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
Best Practices
1. Strategic Placement
Place error boundaries at strategic points in your component tree:
- Around route components
- Around major UI sections
- Around third-party components
2. Granular Error Handling
Don't wrap your entire app in a single error boundary. Use multiple boundaries for different sections:
function App() {
return (
<div>
<Header /> {/* Not wrapped - header errors shouldn't crash navigation */}
<ErrorBoundary>
<Navigation />
</ErrorBoundary>
<main>
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
</main>
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
</div>
);
}
3. Meaningful Error Messages
Provide helpful error messages and recovery options:
<ErrorBoundary
fallbackRender={({ error, reset }) => (
<div>
<h2>Unable to load comments</h2>
<p>There was a problem loading the comments section.</p>
<button onClick={reset}>Retry</button>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
)}
>
<CommentsSection />
</ErrorBoundary>
4. Error Reporting
Always log errors to your monitoring service:
<ErrorBoundary
onError={(error, errorInfo) => {
// Log to your error monitoring service
errorReportingService.captureException(error, {
extra: errorInfo,
tags: { boundary: 'comments-section' }
});
}}
>
<CommentsSection />
</ErrorBoundary>
Testing Error Boundaries
Create a component for testing error scenarios:
function BuggyComponent({ shouldThrowError = false }: { shouldThrowError?: boolean }) {
const [explode, setExplode] = useState(false);
if (shouldThrowError && explode) {
throw new Error('Boom! 💥');
}
return (
<div>
<p>I'm a potentially unstable component...</p>
<button onClick={() => setExplode(true)}>
Trigger Error
</button>
</div>
);
}
React 18 and Concurrent Features
With React 18's concurrent features, error boundaries work seamlessly with:
- Suspense boundaries
- Concurrent rendering
- Automatic batching
Make sure your error boundaries are compatible with these features by avoiding side effects in render methods.
Alternatives and Complementary Patterns
1. React Query Error Handling
For data fetching errors, React Query provides excellent error handling:
function DataComponent() {
const { data, error, isError } = useQuery('userData', fetchUser);
if (isError) {
return <div>Error loading user: {error.message}</div>;
}
return <div>{data?.name}</div>;
}
2. Error Context
For global error state management:
const ErrorContext = createContext<{
reportError: (error: Error) => void;
}>({
reportError: () => {}
});
function ErrorProvider({ children }: { children: ReactNode }) {
const reportError = useCallback((error: Error) => {
// Send to error reporting service
console.error('Global error:', error);
}, []);
return (
<ErrorContext.Provider value={{ reportError }}>
{children}
</ErrorContext.Provider>
);
}
Conclusion
Error boundaries are essential for building robust React applications. They provide a safety net that prevents your entire app from crashing due to unexpected errors, while giving you the opportunity to:
- Display meaningful error messages to users
- Log errors for debugging and monitoring
- Provide recovery mechanisms
- Maintain a better user experience
By implementing error boundaries strategically throughout your application, you create a more resilient user experience that gracefully handles the unexpected.
Key Takeaways
- Always use error boundaries in production React applications
- Place them strategically - not just around your entire app
- Provide meaningful fallback UIs with recovery options
- Log errors to your monitoring service for debugging
- Test error scenarios during development
- Combine with other error handling patterns for comprehensive coverage
Remember: Error boundaries are about graceful degradation, not preventing errors. Focus on providing the best possible user experience when things go wrong.
Top comments (1)
How wasteful some brokers can be, I sold my truck thinking I was taking a great step to greatness, not knowing I was a victim. I lost about $65k to the scammers I'm so grateful to Darek Recovery who helped me. I saw a post about the company here on blogs. I had to contact him , he asked me of proof, then he started the process, within 24 hours I got all my lost funds back. his email address is recoverydarek @ gmail com If you are n need of help they are 100% legit.