Error Boundaries in React
What are Error Boundaries?
Error Boundaries are React components that catch JavaScript errors in their child component tree, log them, and display a fallback UI instead of crashing the entire application.
Why use Error Boundaries?
Before Error Boundaries, an error in one component would break the entire app. Error Boundaries allow you to:
- Prevent the entire application from crashing
- Show users a friendly error message
- Log errors for analysis
- Isolate problematic parts of the application
Creating an Error Boundary
An Error Boundary is a class component with getDerivedStateFromError and/or componentDidCatch methods.
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error to reporting service
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Usage
Wrap components that might throw errors:
function App() {
return (
<div>
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
<ErrorBoundary>
<AnotherWidget />
</ErrorBoundary>
</div>
);
}
If MyWidget throws an error, only it will crash while AnotherWidget continues working.
Difference between methods
getDerivedStateFromError
Called during rendering phase. Used to update state and show fallback UI.
static getDerivedStateFromError(error) {
// Must return an object to update state
return { hasError: true, error };
}
componentDidCatch
Called after rendering. Used for logging errors.
componentDidCatch(error, errorInfo) {
// errorInfo.componentStack contains the component call stack
logErrorToService(error, errorInfo);
}
Advanced Example
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
// Send to monitoring service
logErrorToService(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>An error occurred</h2>
<details>
<summary>Details</summary>
<p>{this.state.error && this.state.error.toString()}</p>
<pre>{this.state.errorInfo && this.state.errorInfo.componentStack}</pre>
</details>
<button onClick={this.handleReset}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
What Error Boundaries DON'T catch
Error Boundaries catch errors in:
- Render methods
- Lifecycle methods
- Constructors of child components
They DON'T catch:
- Event handlers
- Asynchronous code (setTimeout, Promise)
- Server-side rendering errors
- Errors in the Error Boundary itself
Event Handlers
Use regular try-catch for event handlers:
function MyComponent() {
function handleClick() {
try {
// Code that might throw an error
somethingDangerous();
} catch (error) {
console.error('Error in click handler:', error);
}
}
return <button onClick={handleClick}>Click me</button>;
}
Asynchronous Errors
Use try-catch for asynchronous errors as well:
function MyComponent() {
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
// ...
} catch (error) {
console.error('Fetch error:', error);
}
}
useEffect(() => {
fetchData();
}, []);
return <div>...</div>;
}
Where to place Error Boundaries
Global Level
function App() {
return (
<ErrorBoundary>
<Router>
<Layout>
<Routes />
</Layout>
</Router>
</ErrorBoundary>
);
}
Route Level
function App() {
return (
<Router>
<ErrorBoundary>
<Route path="/profile" element={<Profile />} />
</ErrorBoundary>
<ErrorBoundary>
<Route path="/settings" element={<Settings />} />
</ErrorBoundary>
</Router>
);
}
Component Level
function Dashboard() {
return (
<div>
<ErrorBoundary>
<ChartWidget />
</ErrorBoundary>
<ErrorBoundary>
<StatsWidget />
</ErrorBoundary>
</div>
);
}
Integration with monitoring services
Sentry
import * as Sentry from '@sentry/react';
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { contexts: { react: errorInfo } });
}
// ...
}
// Or use Sentry's built-in ErrorBoundary
import { ErrorBoundary } from '@sentry/react';
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<MyApp />
</ErrorBoundary>
);
}
React 18 and useErrorBoundary
React doesn't have a hook for Error Boundaries yet, but libraries offer solutions:
react-error-boundary
import { ErrorBoundary, useErrorHandler } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset app state
}}
onError={(error, errorInfo) => {
// Log error
}}
>
<MyApp />
</ErrorBoundary>
);
}
// Use in functional components
function MyComponent() {
const handleError = useErrorHandler();
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
} catch (error) {
handleError(error);
}
}
// ...
}
Best Practices
Multiple Error Boundaries
Don't use a single global Error Boundary. Use multiple at different levels:
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<Header />
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Content />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
Informative Messages
Show users helpful information:
function ErrorFallback({ error }) {
return (
<div className="error-page">
<h1>Oops! Something went wrong</h1>
<p>We're working on fixing this.</p>
<p>Try:</p>
<ul>
<li>Refreshing the page</li>
<li>Going back to home</li>
<li>Reporting the issue to us</li>
</ul>
{process.env.NODE_ENV === 'development' && (
<details>
<summary>Technical details</summary>
<pre>{error.message}</pre>
</details>
)}
</div>
);
}
Recovery Options
Give users a way to try again:
class ErrorBoundary extends React.Component {
state = { hasError: false, errorCount: 0 };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidUpdate(prevProps) {
// Reset error when children change
if (this.state.hasError && prevProps.children !== this.props.children) {
this.setState({ hasError: false, errorCount: 0 });
}
}
handleReset = () => {
this.setState(state => ({
hasError: false,
errorCount: state.errorCount + 1
}));
};
render() {
if (this.state.hasError) {
return (
<div>
<h1>Error</h1>
<button onClick={this.handleReset}>Try again</button>
{this.state.errorCount > 2 && (
<p>If the problem persists, please contact support.</p>
)}
</div>
);
}
return this.props.children;
}
}
Conclusion
Error Boundaries:
- Catch errors in React components
- Must be class components
- Use
getDerivedStateFromErrorandcomponentDidCatch - Don't catch errors in event handlers and async code
- Best to use multiple at different levels
- Allow showing fallback UI instead of blank screen
- Essential for production applications
In interviews:
Important to be able to:
- Explain what Error Boundaries are
- Show how to create an Error Boundary
- Describe which errors they don't catch
- Explain the difference between
getDerivedStateFromErrorandcomponentDidCatch - Give examples of proper Error Boundary placement