Skip to main content

Error Boundaries

What Are Error Boundaries?

Without error handling, a JavaScript error thrown inside a component crashes the entire React tree — the user sees a blank page. Error Boundaries are components that catch errors anywhere in their child component tree and display a fallback UI instead.

Think of them as a try/catch for your component tree.

Without Error Boundary:
App → Header → Main → BrokenWidget
↓ throws
BLANK PAGE ❌

With Error Boundary:
App → Header → Main → ErrorBoundary → BrokenWidget
↓ catches error
"Something went wrong" ✅ (rest of app works)

Creating an Error Boundary

Error boundaries must be class components — they're the one remaining use case for class components in modern React. You implement componentDidCatch and getDerivedStateFromError:

src/components/ErrorBoundary.jsx
import { Component } from "react";

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error) {
// Called during render — update state to show fallback UI
return { hasError: true, error };
}

componentDidCatch(error, info) {
// Called after render — use for logging
console.error("Error caught by boundary:", error);
console.error("Component stack:", info.componentStack);
// logErrorToService(error, info); // e.g. Sentry
}

render() {
if (this.state.hasError) {
return this.props.fallback || (
<div style={{ padding: "20px", color: "red" }}>
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}

return this.props.children;
}
}

export default ErrorBoundary;

Two lifecycle methods:

  • getDerivedStateFromError — runs during the render phase. Return new state to trigger the fallback UI.
  • componentDidCatch — runs after render. Use this to log the error to a monitoring service.

Using Error Boundaries

Wrap any part of your tree to isolate errors:

import ErrorBoundary from "./components/ErrorBoundary";

function App() {
return (
<div>
<Header />
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
<Footer />
</div>
);
}

Now if MainContent or anything inside it throws, the header and footer remain intact — only the bounded section shows the fallback.

Custom Fallback UI

Pass a fallback prop for custom error UI:

<ErrorBoundary
fallback={
<div className="error-card">
<h3>Failed to load this section</h3>
<p>Please refresh the page or try again later.</p>
</div>
}
>
<UserDashboard />
</ErrorBoundary>

Granular Error Isolation

Place multiple boundaries at different levels for different fallback experiences:

function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Header />
<main>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>

<ErrorBoundary fallback={<ContentError />}>
<MainFeed />
</ErrorBoundary>
</main>
<Footer />
</ErrorBoundary>
);
}

A crash in Sidebar shows <SidebarError /> but MainFeed keeps working.

What Error Boundaries Don't Catch

Error boundaries only catch errors that happen during rendering, lifecycle methods, and constructors of child components. They do not catch:

Not caughtUse instead
Event handlers (onClick, etc.)try/catch inside the handler
Async code (setTimeout, fetch)try/catch inside async functions
Errors in the boundary itselfA parent boundary
Server-side rendering errorsSSR-specific handling
// This is NOT caught by an error boundary
function Button() {
const handleClick = () => {
throw new Error("This won't be caught!"); // use try/catch here
};
return <button onClick={handleClick}>Click</button>;
}

// This IS caught by an error boundary
function BrokenComponent() {
throw new Error("This is caught during render!"); // throws during render
return <div>Never shown</div>;
}

Resetting After an Error

A common pattern — let users retry after an error:

class ErrorBoundary extends Component {
state = { hasError: false };

static getDerivedStateFromError() {
return { hasError: true };
}

componentDidCatch(error, info) {
logError(error, info);
}

reset = () => this.setState({ hasError: false });

render() {
if (this.state.hasError) {
return (
<div>
<p>Something went wrong.</p>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
tip
Use react-error-boundary

The community package react-error-boundary provides a well-tested, flexible ErrorBoundary component with hooks support, so you don't have to write the class component yourself:

npm install react-error-boundary
import { ErrorBoundary } from "react-error-boundary";

function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}

<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { /* reset state */ }}>
<YourComponent />
</ErrorBoundary>

Where to Place Error Boundaries

A good rule of thumb: wrap any section that loads async data or renders user-generated content independently:

// Route-level — each page is isolated
<ErrorBoundary><DashboardPage /></ErrorBoundary>
<ErrorBoundary><ProfilePage /></ErrorBoundary>

// Widget-level — widgets can fail independently
<ErrorBoundary><WeatherWidget /></ErrorBoundary>
<ErrorBoundary><StockTicker /></ErrorBoundary>

// Data-fetching components — API failures stay contained
<ErrorBoundary><UserComments postId={id} /></ErrorBoundary>

Don't wrap every single component — that's overkill and clutters the tree. Wrap logical sections where an isolated failure is acceptable.