Skip to main content

React Error Boundaries: Catch Component Errors

Error boundaries are React class components that catch errors during rendering and allow you to display a fallback UI instead of crashing the entire page. When a child component throws an error, the error boundary intercepts it, logs it to Sentry, and renders a friendly message. Without error boundaries, a single broken component takes down your entire React tree.

What Errors Do Error Boundaries Catch?

Error boundaries only catch errors that occur during the React render phase and lifecycle methods. They do NOT catch:

  • Event handler errors (use try-catch in onClick handlers instead).
  • Async errors in setTimeout, promises, or async/await (handled by Sentry's promise rejection handler).
  • Errors in error boundaries themselves (they propagate up).
  • Server-side rendering errors (SSR has its own error handling).

Error boundaries catch component lifecycle errors: constructor, render, componentDidMount, setState, and getInitialState errors. For React Hooks apps, there is no Hook-based error boundary yet (as of 2026), so you must use class components for the boundary itself.

Creating a Basic Error Boundary

An error boundary is a class component that implements either getDerivedStateFromError() or componentDidCatch() (or both). Here is a minimal example:

import React from "react";

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

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}

componentDidCatch(error, errorInfo) {
// Log the error to an external service like Sentry
console.error("Error caught by boundary:", error, errorInfo);
// Sentry.captureException(error, { contexts: { react: errorInfo } });
}

render() {
if (this.state.hasError) {
return (
div style={{ padding: "20px", border: "1px solid red" }}>
<h1>Something went wrong.</h1>
<p>Error: {this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
Reload page
</button>
/div>
);
}

return this.props.children;
}
}

export default ErrorBoundary;

Wrap your app or a subtree:

<ErrorBoundary>
<YourComponent />
</ErrorBoundary>

Now if YourComponent throws an error during render, the boundary catches it and displays the fallback UI. The error is also sent to Sentry (via componentDidCatch) and logged to the console.

Isolating Errors with Multiple Boundaries

A single error boundary around your entire app is too coarse: if your header crashes, your sidebar and footer also disappear. Instead, wrap high-risk sections individually so failures stay localized. A typical app layout might use boundaries like this:

function App() {
return (
div>
<ErrorBoundary fallbackUI={<HeaderError />}>
<Header />
</ErrorBoundary>
<div style={{ display: "flex" }}>
<ErrorBoundary fallbackUI={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<main>
<ErrorBoundary fallbackUI={<MainError />}>
<MainContent />
</ErrorBoundary>
</main>
</div>
<ErrorBoundary fallbackUI={<FooterError />}>
<Footer />
</ErrorBoundary>
/div>
);
}

With this setup, if the header component crashes, the sidebar, footer, and main content still render. The page remains usable, and you reduce user frustration from cascading failures.

Using Sentry's Error Boundary Wrapper

Rather than writing your own error boundary class, you can use Sentry's drop-in wrapper:

import * as Sentry from "@sentry/react";

const ErrorFallback = ({ error, resetError }) => (
div style={{ padding: "20px" }}>
<h2>Oops! We encountered an error.</h2>
<details style={{ whiteSpace: "pre-wrap" }}>
{error && error.toString()}
</details>
<button onClick={resetError}>Try again</button>
/div>
);

function MyComponent() {
return <h1>Normal content</h1>;
}

const WrappedComponent = Sentry.withErrorBoundary(MyComponent, {
fallback: <ErrorFallback />,
showDialog: false, // set to true to show Sentry error report dialog
});

export default WrappedComponent;

Sentry.withErrorBoundary automatically logs errors to Sentry and renders your custom fallback. The showDialog option prompts users to provide feedback, which is useful for understanding impact.

Handling Errors in Event Handlers and Async Code

Error boundaries do NOT catch errors in event handlers (onClick, onChange, onSubmit). You must use try-catch blocks inside event handlers:

function Button() {
const handleClick = async () => {
try {
await fetchData();
} catch (error) {
Sentry.captureException(error);
// Manually update UI or show a toast
}
};

return button onClick={handleClick}>Click me</button>;
}

For async errors that occur after a component is mounted (in useEffect or a Promise chain), use Sentry's global promise rejection handler (set up in the Sentry initialization step in article 2). Sentry automatically captures unhandled promise rejections, so you do not need to handle them separately in most cases.

However, if you want to catch a promise rejection locally and provide a recovery mechanism:

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/react";

function DataFetcher() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
let isMounted = true;

const fetchData = async () => {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (isMounted) setData(json);
} catch (err) {
if (isMounted) {
setError(err.message);
Sentry.captureException(err);
}
}
};

fetchData();

return () => {
isMounted = false; // prevent state updates after unmount
};
}, []);

if (error) {
return div>Error: {error} /div>;
}

return div>{data ? JSON.stringify(data) : "Loading..."} /div>;
}

Error Recovery Patterns

An error boundary is only useful if the user can recover. Common recovery patterns:

Reload the page:

button onClick={() => window.location.reload()}>Reload</button>

Reset to a safe state:

// In a custom hook
const [, setErrorState] = useState(null);
const reset = () => setErrorState(null);

Retry the failed operation:

const [retryCount, setRetryCount] = useState(0);
const handleRetry = () => setRetryCount(retryCount + 1);

Navigate to a safe page:

import { useNavigate } from "react-router-dom";

const navigate = useNavigate();
const handleNavigateHome = () => navigate("/");

Choose recovery mechanisms that make sense for your app. A dashboard should offer "reload" or "go home." A checkout page should let users contact support.

Key Takeaways

  • Error boundaries catch render-phase errors in child components and display a fallback UI.
  • They do NOT catch event handler errors, async errors, or errors in the boundary itself.
  • Wrap high-risk sections individually (header, sidebar, main content) so failures stay localized.
  • Use getDerivedStateFromError for rendering fallback UI and componentDidCatch for logging to Sentry.
  • Pair error boundaries with event handler try-catch blocks and async error handling for complete coverage.

Frequently Asked Questions

Can I use error boundaries in functional components?

No, not yet (as of 2026). Error boundaries must be class components because they rely on getDerivedStateFromError and componentDidCatch lifecycle methods. A Hook-based error boundary is on the React roadmap but not yet stable. Use Sentry's withErrorBoundary HOC wrapper as a workaround in functional apps.

Do error boundaries affect performance?

No. Error boundaries are only active if an error occurs. The render overhead is negligible because it is just a conditional check in the render method. The memory overhead is a few kilobytes per boundary.

What happens if an error boundary itself throws an error?

The error propagates up to its parent error boundary. If no parent boundary catches it, the page breaks and Sentry's global error handler captures it. Avoid complex logic in error boundary render methods to minimize this risk.

Can I show a different fallback UI based on the error type?

Yes. In getDerivedStateFromError, you can check the error message or type and store different state:

static getDerivedStateFromError(error) {
if (error.message.includes("Network")) {
return { hasError: true, errorType: "network" };
}
return { hasError: true, errorType: "unknown" };
}

Then render different UI based on this.state.errorType.

Should I use error boundaries for every component?

No. Use error boundaries for top-level sections (header, sidebar, main content, footer) and for risky components that frequently break (charts, third-party widgets). Wrapping every component is overkill and clutters your component tree.

Further Reading