Error Boundary
Problem
Uncaught JavaScript errors crash the entire application, unmounting the entire component tree and leaving users staring at a blank white screen with no explanation or way to recover. A single broken component in a sidebar widget or footer element can take down the entire page, destroying all user state and forcing a full page reload even when the primary functionality is working correctly, while users lose unsaved form data, active selections, scroll positions, and any in-progress work when errors occur.
Without error boundaries, every component error propagates up to the root, causing catastrophic failure. Errors that occur in third-party components or during data transformation crash the application with no way to contain the damage, while the browser console shows stack traces but users see nothing helpful. Development errors that slip through testing take down production applications completely, while network failures or malformed API responses that cause rendering errors crash entire pages rather than just the affected section.
Solution
Catch component errors during rendering, lifecycle methods, and child component constructors, then display fallback UI instead of crashing the entire application.
Wrap sections of the component tree in error boundary components that intercept errors thrown by descendants and render alternative UI while keeping sibling and parent components functional. This contains failures to affected sections while preserving the rest of the application.
Error boundaries can log errors to monitoring services like Sentry, Rollbar, or LogRocket for debugging production issues. They can provide reset mechanisms that allow users to retry failed operations or clear error state.
Strategic placement of error boundaries creates resilience layers - page-level boundaries prevent total crashes, section-level boundaries isolate features, and component-level boundaries contain individual widgets. Error boundaries do not catch errors in event handlers, async code, or server-side rendering, requiring separate error handling for those cases.
Example
This example demonstrates an error boundary component that catches rendering errors and displays fallback UI instead of crashing the entire application.
React Error Boundary with Logging
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// Called when a child component throws an error during rendering
static getDerivedStateFromError(error) {
// Update state to trigger fallback UI
return { hasError: true, error };
}
// Called after error has been captured
componentDidCatch(error, errorInfo) {
// Log error to monitoring service
console.error('Error caught by boundary:', error, errorInfo);
// Send to error tracking service
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { react: { componentStack: errorInfo.componentStack } }
});
}
}
render() {
if (this.state.hasError) {
// Show fallback UI if an error was caught
return (
<div className="error-boundary">
<h1>Something went wrong</h1>
<p>We've been notified and are looking into it.</p>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
);
}
// Render children normally if no error
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
);
}
Error Boundary with Reset
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
resetError = () => {
// Clear error state to retry rendering
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div>
<h1>Something went wrong</h1>
<button onClick={this.resetError}>Try again</button>
</div>
);
}
return this.props.children;
}
}
Granular Error Boundaries
function Dashboard() {
return (
<div>
{/* Page-level boundary prevents total crashes */}
<ErrorBoundary fallback={<PageError />}>
{/* Section-level boundaries isolate features */}
<ErrorBoundary fallback={<WidgetError />}>
<SalesChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}>
<RecentActivity />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}>
<UserList />
</ErrorBoundary>
</ErrorBoundary>
</div>
);
}
Custom Fallback UI
function ErrorBoundary({ children, fallback, onError }) {
const [error, setError] = useState(null);
if (error) {
// Render custom fallback component
return typeof fallback === 'function'
? fallback(error, () => setError(null))
: fallback;
}
return children;
}
// Usage with custom fallback
<ErrorBoundary
fallback={(error, reset) => (
<div>
<h2>Widget failed to load</h2>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<ComplexWidget />
</ErrorBoundary>
React Error Boundary Library
import { ErrorBoundary } 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() {
const handleError = (error, errorInfo) => {
// Log to error tracking service
logErrorToService(error, errorInfo);
};
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={handleError}
onReset={() => {
// Reset app state if needed
}}
>
<Dashboard />
</ErrorBoundary>
);
}
Vue Error Boundary
<template>
<div v-if="hasError" class="error-boundary">
<h1>Something went wrong</h1>
<p>{{ errorMessage }}</p>
<button @click="reset">Try again</button>
</div>
<slot v-else></slot>
</template>
<script>
export default {
name: 'ErrorBoundary',
data() {
return {
hasError: false,
errorMessage: ''
};
},
// Catch errors from child components
errorCaptured(err, instance, info) {
this.hasError = true;
this.errorMessage = err.message;
// Log to error tracking service
if (window.Sentry) {
window.Sentry.captureException(err, {
contexts: { vue: { componentName: instance.$options.name, info } }
});
}
// Prevent error from propagating further
return false;
},
methods: {
reset() {
this.hasError = false;
this.errorMessage = '';
}
}
};
</script>
Vue Composition API
<script setup>
import { ref, onErrorCaptured } from 'vue';
const hasError = ref(false);
const error = ref(null);
onErrorCaptured((err, instance, info) => {
hasError.value = true;
error.value = err;
console.error('Error captured:', err, info);
// Prevent propagation
return false;
});
function reset() {
hasError.value = false;
error.value = null;
}
</script>
<template>
<div v-if="hasError">
<h1>Error occurred</h1>
<p>{{ error.message }}</p>
<button @click="reset">Try again</button>
</div>
<slot v-else />
</template>
Svelte Error Handling
<script>
import { onMount } from 'svelte';
let hasError = false;
let errorMessage = '';
// Global error handler
onMount(() => {
const handleError = (event) => {
if (event.error) {
hasError = true;
errorMessage = event.error.message;
event.preventDefault();
}
};
window.addEventListener('error', handleError);
return () => {
window.removeEventListener('error', handleError);
};
});
function reset() {
hasError = false;
errorMessage = '';
}
</script>
{#if hasError}
<div class="error-boundary">
<h1>Something went wrong</h1>
<p>{errorMessage}</p>
<button on:click={reset}>Try again</button>
</div>
{:else}
<slot />
{/if}
Web Component Error Boundary
class ErrorBoundary extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.hasError = false;
this.errorMessage = '';
}
connectedCallback() {
this.render();
// Listen for error events from child elements
this.addEventListener('error', (event) => {
this.hasError = true;
this.errorMessage = event.error?.message || 'Unknown error';
this.render();
event.stopPropagation();
// Log to monitoring service
console.error('Error boundary caught:', event.error);
}, true);
// Also handle unhandled promise rejections
this.addEventListener('unhandledrejection', (event) => {
this.hasError = true;
this.errorMessage = event.reason?.message || 'Promise rejection';
this.render();
event.preventDefault();
});
}
reset() {
this.hasError = false;
this.errorMessage = '';
this.render();
}
render() {
if (this.hasError) {
this.shadowRoot.innerHTML = `
<div class="error-boundary">
<h1>Something went wrong</h1>
<p>${this.errorMessage}</p>
<button id="reset">Try again</button>
</div>
`;
this.shadowRoot.getElementById('reset').addEventListener('click', () => this.reset());
} else {
this.shadowRoot.innerHTML = '<slot></slot>';
}
}
}
customElements.define('error-boundary', ErrorBoundary);
Conditional Error Boundaries
function App() {
return (
<div>
{/* Only use error boundary in production */}
{process.env.NODE_ENV === 'production' ? (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
) : (
<Dashboard />
)}
</div>
);
}
Benefits
- Prevents entire application crashes from isolated component errors, keeping the application partially functional instead of showing a blank screen.
- Provides graceful degradation with fallback UI for broken sections, showing users helpful error messages instead of cryptic browser errors.
- Isolates failures to specific parts of the UI tree, containing damage so one broken widget doesn’t destroy the entire page.
- Can log errors to monitoring services for debugging production issues, providing stack traces and context that help developers fix bugs.
- Improves user experience by keeping functional parts working, allowing users to continue using unaffected features.
- Enables progressive disclosure of errors - show simple messages to users while logging detailed technical information for developers.
- Provides retry mechanisms that let users attempt to recover from transient failures without reloading the page.
Tradeoffs
- Only catches errors in render, lifecycle methods, and constructors - does not catch errors in event handlers, async code like setTimeout or promises, server-side rendering, or errors thrown in the error boundary itself.
- Requires React class components - not available as hooks, forcing functional component codebases to maintain class components specifically for error boundaries or use third-party libraries.
- Can hide bugs if fallback UI is too generic or unclear - users see “something went wrong” without understanding what broke or how to proceed, and developers may not investigate if errors seem handled.
- May mask underlying issues that should be fixed - error boundaries treat symptoms rather than causes, and poorly placed boundaries might catch errors that should propagate for proper handling.
- Requires strategic placement to balance granularity and coverage - too few boundaries create coarse error handling that takes down large sections, while too many create complex error UI and recovery logic.
- Event handler errors require separate try-catch blocks - developers must remember to wrap event handlers manually since error boundaries don’t catch those errors automatically.
- Async errors in useEffect or data fetching require explicit error state management - error boundaries don’t catch promise rejections or errors in async functions.
- Error boundaries catch errors during render but the error state itself can cause new errors if fallback UI has bugs, potentially creating error loops that are difficult to debug.
- Reset functionality requires careful consideration of what state to reset - simply clearing error state may cause the same error to occur again if underlying conditions haven’t changed.
- Multiple nested error boundaries create complexity in determining which boundary catches which errors and what fallback UI displays where.
- Testing error boundaries requires intentionally throwing errors in tests, which can be awkward and may require disabling console.error to avoid test output pollution.
- Error boundary placement decisions are difficult - should each component have its own boundary, or should boundaries wrap larger sections? The right answer depends on the application structure and failure modes.
- Production error boundaries may catch errors that would help developers debug in development, requiring conditional boundaries or error rethrowing in development environments.