TL;DR: An error boundary is a React component that catches JavaScript errors thrown by components inside it. Without one, a single component error wipes your entire page blank. AI adds error boundaries as a safety net around anything that could plausibly fail — data-fetching components, third-party widgets, user-generated content. They are the React equivalent of a circuit breaker: one section trips, the rest of the app stays on.
Why AI Coders Need to Know This
You opened Cursor, described a feature, and got back a component wrapped in something like this:
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<UserDashboard userId={userId} />
</ErrorBoundary>
There might also be an import at the top: import { ErrorBoundary } from 'react-error-boundary'. You did not ask for any of this. You are not sure if it matters. And part of you wants to strip it out to keep the code clean.
Here is why you should not.
In React, when a component throws an error during rendering — maybe it received unexpected data, maybe a third-party library exploded, maybe the API returned something it did not expect — React's default behavior is brutal: it unmounts the entire component tree. The whole page goes blank. In production, the user sees nothing. No error message, no fallback, just white.
An error boundary is the mechanism React provides to prevent that. It catches errors that bubble up from child components and displays a fallback UI instead of letting the crash propagate. Think of it as a firewall between a burning room and the rest of the building.
AI adds them because it knows this failure mode exists and it is trying to ship you production-ready code. Understanding what they do — and when you actually need them — makes you a better judge of AI output and a more confident editor of the code it generates.
The Real-World Scenario
You are building a SaaS dashboard. You open Cursor and type:
Prompt I Would Type
Build a React dashboard page with three widgets: recent activity feed,
account stats summary, and a list of the user's recent invoices.
Each widget fetches its own data from a separate API endpoint.
Use Next.js App Router. Make it production-ready.
Cursor comes back with a page component, three separate widget components, and a structure you did not quite expect. Each widget is not just wrapped in <Suspense> — it is also wrapped in an <ErrorBoundary>. You have three of them, nested together:
<ErrorBoundary fallback={<WidgetError message="Activity feed unavailable" />}>
<Suspense fallback={<WidgetSkeleton />}>
<ActivityFeed userId={userId} />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError message="Stats unavailable" />}>
<Suspense fallback={<WidgetSkeleton />}>
<AccountStats userId={userId} />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError message="Invoices unavailable" />}>
<Suspense fallback={<WidgetSkeleton />}>
<InvoiceList userId={userId} />
</Suspense>
</ErrorBoundary>
This looks complicated. But it is actually exactly right. Each widget is isolated — if the invoice API is down, the other two widgets still render fine. The AI is building you a resilient dashboard, not a fragile one. Let's understand exactly how.
What AI Generated
Here is a complete, realistic error boundary setup that AI commonly produces. Read the inline comments — every piece is explained.
// app/dashboard/page.tsx
// React Server Component — runs on the server
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// react-error-boundary is a small library (5KB) that wraps React's
// error boundary API in a much friendlier interface
// Install with: npm install react-error-boundary
// ─── Fallback Components ─────────────────────────────────────────────────────
// Shown when a widget's data fetch throws an error
// "error" and "resetErrorBoundary" are injected by react-error-boundary
function WidgetError({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div className="widget-error">
<p>This section is temporarily unavailable.</p>
<p className="error-detail">{error.message}</p>
{/* "Try again" calls resetErrorBoundary, which re-renders the widget */}
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Shown while a widget is fetching its data (Suspense fallback)
function WidgetSkeleton() {
return <div className="widget-skeleton" aria-busy="true" />;
}
// ─── Data-Fetching Widgets ───────────────────────────────────────────────────
// Each widget is an async Server Component — it fetches its own data
// If the fetch throws, the nearest ErrorBoundary above it catches it
async function ActivityFeed({ userId }: { userId: string }) {
const res = await fetch(`/api/activity?userId=${userId}`);
// Throwing here causes the ErrorBoundary to catch and show WidgetError
if (!res.ok) throw new Error(`Activity feed failed: ${res.status}`);
const activity = await res.json();
return (
<ul>
{activity.map((item: ActivityItem) => (
<li key={item.id}>{item.description}</li>
))}
</ul>
);
}
async function AccountStats({ userId }: { userId: string }) {
const res = await fetch(`/api/stats?userId=${userId}`);
if (!res.ok) throw new Error(`Stats failed: ${res.status}`);
const stats = await res.json();
return <div>{/* render stats */}</div>;
}
async function InvoiceList({ userId }: { userId: string }) {
const res = await fetch(`/api/invoices?userId=${userId}`);
if (!res.ok) throw new Error(`Invoices failed: ${res.status}`);
const invoices = await res.json();
return <ul>{/* render invoices */}</ul>;
}
// ─── Page ────────────────────────────────────────────────────────────────────
export default async function DashboardPage() {
const userId = 'user_123'; // from auth in a real app
return (
<main>
<h1>Your Dashboard</h1>
<div className="dashboard-grid">
{/*
ErrorBoundary wraps the widget to catch rendering/fetch errors.
Suspense wraps the widget to show a skeleton while data loads.
Order matters: ErrorBoundary goes OUTSIDE Suspense.
This way, errors from both the loading phase and the render phase
are caught by the same boundary.
*/}
<ErrorBoundary FallbackComponent={WidgetError}>
<Suspense fallback={<WidgetSkeleton />}>
<ActivityFeed userId={userId} />
</Suspense>
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetError}>
<Suspense fallback={<WidgetSkeleton />}>
<AccountStats userId={userId} />
</Suspense>
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetError}>
<Suspense fallback={<WidgetSkeleton />}>
<InvoiceList userId={userId} />
</Suspense>
</ErrorBoundary>
</div>
</main>
);
}
This is about 80 lines. It looks like a lot but is doing something genuinely useful. Let's break down each part.
Understanding Error Boundaries
What an Error Boundary Actually Does
React's rendering model has one critical weakness: if any component in the tree throws an error while React is building the UI, React panics and unmounts the whole tree. Your page goes blank. No partial rendering, no graceful degradation — just nothing.
An error boundary intercepts that panic. When a component inside it throws, the error boundary catches the error, stays mounted, and renders its fallback instead of the failed child. Every other part of the page that is outside the boundary continues to work normally.
Here is the key mental model: error boundaries are like circuit breakers. An electrical circuit breaker does not prevent a short circuit — it contains it. When one circuit trips, the rest of the house stays powered. An error boundary does the same thing for your React component tree.
The Two Flavors: Class Component vs react-error-boundary
React's built-in error boundary API requires a class component. This is the only remaining use case for class components in modern React — hooks cannot do it. You will rarely need to write one from scratch, but it is useful to recognize it when AI generates it:
// The raw React class component approach
// You will see this in older codebases or when AI targets older React
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
// React calls this when a child throws during rendering
// Return new state to show the fallback UI
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Called after an error is caught — good place to log to an error service
componentDidCatch(error, errorInfo) {
console.error('Caught by ErrorBoundary:', error, errorInfo);
// logErrorToService(error, errorInfo); // Sentry, Datadog, etc.
}
render() {
if (this.state.hasError) {
// Render fallback instead of the crashed component
return <p>Something went wrong.</p>;
}
// Otherwise render children normally
return this.props.children;
}
}
Writing this every time is tedious. That is why the react-error-boundary library exists — it wraps this class component logic in a clean functional API that AI almost always reaches for in new projects:
// The react-error-boundary library approach
// Much cleaner — what AI generates in modern codebases
import { ErrorBoundary } from 'react-error-boundary';
// FallbackComponent receives the error and a reset function automatically
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Use it like any other wrapper component
<ErrorBoundary FallbackComponent={ErrorFallback}>
<YourComponent />
</ErrorBoundary>
The library version gives you two things the raw class component lacks: a resetErrorBoundary function (so users can retry), and a useErrorBoundary hook (so you can manually send errors from hooks or async code to the boundary).
Where Error Boundaries Live in the Component Tree
The placement of an error boundary determines how much of your UI survives an error. The higher up in the tree you place it, the more it protects — but the more of the page it replaces if something breaks.
// Option 1: Single top-level boundary
// Catches everything — but if it triggers, your whole page is replaced
<ErrorBoundary fallback={<AppCrash />}>
<App />
</ErrorBoundary>
// Option 2: Per-section boundaries (what AI generated in the dashboard example)
// Each section is isolated — one failure does not affect the others
// This is the right pattern for dashboards, feeds, and multi-widget pages
<ErrorBoundary fallback={<SectionError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError />}>
<MainContent />
</ErrorBoundary>
AI tends to default to per-component boundaries when it generates complex pages. This is conservative and often correct — but it can result in more boundaries than you need. You can consolidate them if the components inside are low-risk and not independently useful.
The Reset Mechanism
One of the most practical features of react-error-boundary is reset. When an error boundary catches an error, it is "tripped" — it stays showing the fallback. The resetErrorBoundary function resets the boundary state and re-renders the children from scratch. This gives users a "Try again" button that can recover from transient failures like a brief network outage:
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-fallback">
<p>Failed to load this section.</p>
<p className="error-detail">{error.message}</p>
{/* Clicking this re-renders the child component from zero */}
<button onClick={resetErrorBoundary}>
Try again
</button>
</div>
);
}
// You can also reset based on a key change — useful when props change
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[userId]} // Re-renders when userId changes
>
<UserProfile userId={userId} />
</ErrorBoundary>
The resetKeys prop is particularly useful. If a user navigates to a different profile and the previous one had errored, resetKeys={[userId]} automatically resets the boundary when the key changes. AI sometimes generates this; now you know why.
Error Boundaries vs Try-Catch
This is one of the most common points of confusion. Both catch errors — so what is the difference?
| Situation | Use This |
|---|---|
| Error in component render (JSX returning bad data) | Error Boundary |
| Error in async fetch (API call failed) | try-catch (or throw to trigger boundary) |
| Error in event handler (button click failed) | try-catch inside the handler |
| Error in useEffect | try-catch or useErrorBoundary hook |
| Error in server-side code (API route, server action) | try-catch in the server function |
The rule is clean: try-catch works in regular JavaScript code — functions, async operations, event handlers. Error boundaries work in React's render phase — the part where components return JSX. They cover different territory and you need both in a real app.
The practical flow looks like this: a fetch() call inside an async Server Component throws when the API returns a 500. You are in async code, so React does not catch it automatically — but since you are in a Server Component and you let the error propagate (or throw manually), React treats the component as failed and hands it to the nearest error boundary. The boundary catches the render failure and shows the fallback. The error handling guide goes deeper on this flow.
React 19 and Error Handling
React 19 introduced improvements to how errors are reported alongside the new Suspense streaming model. The onCaughtError, onUncaughtError, and onRecoverableError options on createRoot give you global hooks for logging errors to services like Sentry. Error boundaries themselves work the same way — what changed is the ergonomics of the reporting pipeline around them.
What AI Gets Wrong About Error Boundaries
AI applies error boundaries more confidently than it applies them correctly. Here are the failure modes to watch for.
Adding a boundary but never installing the library
AI often generates import { ErrorBoundary } from 'react-error-boundary' without mentioning that you need to install the package. If you see this import and your project does not have react-error-boundary in package.json, run:
npm install react-error-boundary
Without it, your app will throw a module-not-found error the moment it renders. Check your package.json dependencies first.
Putting the fallback inline as a JSX string
The react-error-boundary API has two slightly different prop names and AI mixes them up occasionally:
// WRONG — fallback prop expects a React element, not a component reference
<ErrorBoundary FallbackComponent="Something went wrong">
<Widget />
</ErrorBoundary>
// CORRECT — FallbackComponent receives the component itself
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Widget />
</ErrorBoundary>
// ALSO CORRECT — fallback prop receives a rendered element
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Widget />
</ErrorBoundary>
Use FallbackComponent when you need access to the error message or the reset function in your fallback. Use fallback for simple static messages.
Forgetting that error boundaries do not catch async errors by default
The most common misunderstanding: error boundaries do not automatically catch errors thrown inside useEffect, event handlers, or async functions. They only catch errors in the render phase.
'use client';
import { useEffect } from 'react';
import { useErrorBoundary } from 'react-error-boundary';
function DataWidget({ userId }: { userId: string }) {
const { showBoundary } = useErrorBoundary();
const [data, setData] = useState(null);
useEffect(() => {
fetch(`/api/data?userId=${userId}`)
.then(r => {
if (!r.ok) throw new Error(`Fetch failed: ${r.status}`);
return r.json();
})
.then(setData)
.catch(error => {
// Without this, the error is swallowed silently
// showBoundary manually sends it to the ErrorBoundary above
showBoundary(error);
});
}, [userId, showBoundary]);
if (!data) return null;
return <div>{/* render data */}</div>;
}
The useErrorBoundary hook from react-error-boundary is the bridge between async errors and your boundary. AI sometimes forgets to include it when generating client components with useEffect fetching. Check for this gap in AI-generated code.
Using error boundaries in server actions
Server actions in Next.js run on the server, not in React's rendering pipeline. Error boundaries have no effect on them. Handle errors from server actions with try-catch in the action itself and return structured error responses to the client. AI occasionally wraps server action calls in error boundaries expecting them to catch server-side failures — they will not.
Quick Checklist for AI-Generated Error Boundaries
When AI generates error boundaries in your code: confirm react-error-boundary is in your package.json, check whether the prop is fallback (element) or FallbackComponent (component), look for async code inside useEffect that might need useErrorBoundary, and verify the boundary is outside (not inside) any Suspense wrapper.
How to Debug Error Boundary Issues
Error boundary bugs fall into three categories: the boundary never triggers, the boundary triggers on every render, or the fallback shows but the reset button does not work.
The fallback never shows — the page goes blank instead
The error boundary is not catching the error. Most likely causes:
- The error is happening in an event handler or async function (outside the render phase). Use
useErrorBoundaryto forward it. - The error is happening above the boundary in the component tree, not inside it.
- You are using the raw class component pattern but the class is not properly extending
React.Component.
To confirm: open the browser console. React logs errors it catches to the console even when an error boundary handles them. If you see the error in the console but not the fallback UI, the boundary is set up incorrectly.
The fallback shows on every render, even with good data
Something in the component is throwing during normal rendering. Common culprits: trying to access a property on undefined (user.name when user is null), calling a method that does not exist, or a key prop receiving an invalid value. Add console.log statements at the top of the component to identify what is undefined when it should not be.
In development, React shows a red overlay with the exact error and stack trace. Read it — it will tell you the exact line number. Do not dismiss the overlay; it is your fastest diagnostic tool.
The reset button does not recover the component
The error is re-thrown on every re-render — meaning the root cause has not been fixed, just the error boundary state. resetErrorBoundary clears the boundary and tries to render the child again. If the child immediately errors again, the fallback reappears. Fix the underlying error, not just the boundary state.
If the error is transient (a brief network failure), the reset should work after the condition clears. If it is deterministic (bad data, missing prop), you need to fix the data flow before the reset helps.
Using React DevTools to inspect boundaries
Install the React DevTools browser extension. In the Components tab, error boundaries appear in the component tree. When one is tripped, DevTools marks it with a warning indicator. You can see exactly which boundary caught the error and what the error state is. This is far faster than reading stack traces when you have nested boundaries.
Logging errors to an error tracking service
In production, you need to know when error boundaries trip. The onError prop on ErrorBoundary from react-error-boundary receives the error and lets you log it:
import * as Sentry from '@sentry/react';
function logError(error: Error, info: { componentStack: string }) {
// Log to Sentry, Datadog, LogRocket, etc.
Sentry.captureException(error, { extra: info });
}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logError}
>
<Widget />
</ErrorBoundary>
Without this, production error boundaries are silent — users see the fallback but you have no record that anything went wrong. AI rarely includes this logging step. Add it for any boundary that wraps significant functionality.
Debugging Tip
In development, React always logs caught errors to the console even when an error boundary handles them. If you see "The above error occurred in the
FAQ
An error boundary is a React component that catches JavaScript errors thrown by any component inside it during rendering. Instead of letting the error crash your whole app and show a blank white screen, it catches the error and displays a fallback UI you define — like a friendly "Something went wrong" message with a retry button.
AI adds error boundaries because in React, an unhandled error in any component will unmount the entire component tree — meaning the whole page goes blank. Error boundaries are the recommended defense. AI applies them proactively around components that fetch data, render user content, or do anything that could plausibly fail. It is trying to ship you code that behaves gracefully in production.
If any component inside that ErrorBoundary throws an error during rendering, React will unmount the entire component tree and the user will see a blank white page. In development, React shows a red error overlay first. In production, the screen just goes blank with no explanation. That is why the boundary is there — it contains the damage to just the failed section.
try-catch works in regular JavaScript functions and handles errors in event handlers, async code, and server-side logic. Error boundaries work only in React's rendering phase — they catch errors that happen while React is building the UI from JSX. You need both in a real app: try-catch handles async errors like failed API calls; error boundaries handle rendering errors like a component receiving unexpected null data.
Error boundaries do not catch errors thrown inside useEffect, event handlers, or async functions — those need try-catch. Error boundaries only catch errors thrown during the render phase. The react-error-boundary library provides a useErrorBoundary hook that lets you manually forward errors from hooks and async code up to the nearest error boundary, bridging the gap between async failures and your boundary's fallback UI.
What to Learn Next
Error boundaries are one piece of a larger error handling story in React. These are the highest-leverage concepts to understand next — each one connects directly to what you just learned.
- What Is React? — the foundation everything here builds on. Understanding the component tree helps you understand why a single error can take down the whole page.
- What Is React Suspense? — Suspense and error boundaries are almost always used together. Suspense handles the "waiting" state; error boundaries handle the "failed" state. You need both in production.
- What Is Try-Catch? — the JavaScript error handling mechanism that works everywhere regular code runs. Error boundaries and try-catch cover complementary territory.
- What Is Error Handling? — the full picture of error handling strategy in React apps: where to catch, what to log, how to recover, and when to let errors bubble.
- What Are React Hooks? — the
useErrorBoundaryhook from react-error-boundary extends your boundary to cover async and event-driven errors. Understanding hooks makes this pattern click.