Error Boundary
Catch JavaScript errors in component trees and display fallback UI gracefully.
In this article
Error Boundary
Problem
A simple typo in a sidebar widget, a malformed API response, or a third-party library throwing an unexpected exception can unmount your entire component tree and leave users staring at a blank screen.
I’ve seen users lose entire forms, along with everything they’d typed, because an unrelated chart component failed to render. The browser console shows a helpful stack trace for developers, but users see nothing useful.
This happens because errors bubble up through your component tree until they hit the root, at which point everything crashes together. One broken footer widget shouldn’t take down the entire page, but without error boundaries it does.
Solution
Think of error boundaries as try-catch blocks for your component tree. You wrap sections of your UI in these special components that catch render errors and display fallback UI instead of crashing.
Everything outside the boundary keeps working. If your sales chart throws an error, users can still see the navigation and sidebar because you’ve contained the blast radius to the chart area.
I recommend a layered approach: put an error boundary at the app root as a safety net, then add granular boundaries around risky features like dashboard widgets, third-party integrations, and user-generated content.
One caveat: React’s error boundaries only catch errors during rendering, lifecycle methods, and constructors. They won’t catch event handlers, async code, or promise rejections, and Svelte’s boundaries have the same limits. Vue’s onErrorCaptured also sees errors from event handlers and watchers that Vue calls, but code in a setTimeout or a promise nobody awaits still needs its own try-catch.
Example
React and Vue have built-in hooks for this, and Svelte 5 has the <svelte:boundary> element. Web Components have no equivalent, because the browser gives custom elements no way to catch errors thrown while their children render, so that version relies on children reporting their own errors.
Basic Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
resetError = () => this.setState({ hasError: false, error: null });
render() {
if (this.state.hasError) {
const { fallback } = this.props;
if (typeof fallback === 'function') {
return fallback(this.state.error, this.resetError);
}
return fallback ?? (
<div className="error-boundary">
<h1>Something went wrong</h1>
<button onClick={this.resetError}>Try again</button>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
);
} <script setup>
import { ref, onErrorCaptured } from 'vue';
const hasError = ref(false);
const error = ref(null);
onErrorCaptured((err) => {
hasError.value = true;
error.value = err;
return false; // Prevent propagation
});
const reset = () => {
hasError.value = false;
error.value = null;
};
</script>
<template>
<div v-if="hasError" class="error-boundary">
<h1>Something went wrong</h1>
<button @click="reset">Try again</button>
</div>
<slot v-else />
</template> <script>
let { children } = $props();
</script>
<svelte:boundary onerror={(error) => console.error('Error caught:', error)}>
{@render children()}
{#snippet failed(error, reset)}
<div class="error-boundary">
<h1>Something went wrong</h1>
<button onclick={reset}>Try again</button>
</div>
{/snippet}
</svelte:boundary> class ErrorBoundary extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.hasError = false;
// Children catch their own errors and report them with a bubbling event
this.addEventListener('component-error', (event) => {
event.stopPropagation();
console.error('Error caught:', event.detail);
this.hasError = true;
this.render();
});
}
connectedCallback() {
this.render();
}
reset() {
this.hasError = false;
this.render();
}
render() {
if (this.hasError) {
this.shadowRoot.innerHTML = `
<div class="error-boundary">
<h1>Something went wrong</h1>
<button id="reset">Try again</button>
</div>
`;
this.shadowRoot.getElementById('reset').onclick = () => this.reset();
} else {
this.shadowRoot.innerHTML = '<slot></slot>';
}
}
}
customElements.define('error-boundary', ErrorBoundary);
// In a child element's render method:
// try {
// this.renderChart();
// } catch (error) {
// this.dispatchEvent(new CustomEvent('component-error', {
// detail: error, bubbles: true, composed: true
// }));
// } Granular Boundaries
Wrapping individual widgets isolates failures so one broken component doesn’t take down the entire dashboard:
function Dashboard() {
return (
<ErrorBoundary fallback={<PageError />}>
<ErrorBoundary fallback={<WidgetError />}>
<SalesChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}>
<RecentActivity />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}>
<UserList />
</ErrorBoundary>
</ErrorBoundary>
);
}
Custom Fallback UI
A render prop pattern allows tailored fallback UI with access to the error and reset function:
<ErrorBoundary
fallback={(error, reset) => (
<div>
<h2>Widget failed to load</h2>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<ComplexWidget />
</ErrorBoundary>
Using react-error-boundary
The react-error-boundary library provides a functional API so you don’t need to write class components:
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>
);
}
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logErrorToService}>
<Dashboard />
</ErrorBoundary>
Benefits
- Your application stays partially functional when things break. Users can keep using navigation and unaffected features even when one component fails.
- Users see helpful fallback UI instead of a blank screen; “This widget failed to load” is much better than a blank white page.
- You contain the blast radius, so a broken chart doesn’t take down the entire dashboard.
- With the boundary connected to Sentry or another monitoring tool, errors are logged automatically and you hear about production errors as they happen.
- Retry buttons let users recover without refreshing the page, which helps with transient failures that only need a second attempt.
- You can show friendly messages to users while logging detailed stack traces for developers.
Tradeoffs
- Error boundaries mostly catch errors during rendering and lifecycle methods. Event handlers (in React and Svelte), async code, and promise rejections still need regular try-catch.
- In React, error boundaries require class components, though
react-error-boundarylets you avoid writing classes yourself. - Generic “Something went wrong” messages can hide bugs from the team, so the boundary needs to log enough detail to debug them.
- Placement is tricky: too few boundaries means large sections crash together, too many means fallback UI everywhere.
- Reset needs care, because if you clear the error state without fixing the cause, the same error might happen again straight away.
Summary
Error boundaries catch JavaScript errors in component trees and display fallback UI instead of crashing the entire application. With boundaries placed around risky code, a failure stays in one small part of the page and the rest keeps working. In production, that means one broken widget shows a fallback instead of the whole page going blank.
See also
- Error Handling — Resilience
- Async Boundary — Resilience
- Loading State — Resilience
- Error Boundaries - React
- errorCaptured Hook - Vue
- Error Boundaries - Svelte
- react-error-boundary Library