React applications render declaratively — your components describe what the UI should look like, and React figures out how to make it happen. But what happens when a component throws a JavaScript error during that rendering process? Without any protection, React has no choice: it unmounts the entire component tree and your user sees a blank screen.
Error Boundaries work like a JavaScript catch {} block, but for components. You wrap a section of your UI in an Error Boundary, and if anything inside that section throws during rendering, the boundary catches it and renders a fallback UI instead of crashing the whole page. The rest of your app keeps working.
By default, if your application throws an error during rendering, React will remove its UI from the screen. To prevent this, you can wrap a part of your UI into an Error Boundary — a special component that lets you display some fallback UI instead of the part that crashed.
https://www.youtube.com/watch?v=OQQAv8t3bfc
Here's the awkward part: there is currently no way to write an Error Boundary as a function component. Everything else in modern React is written as function components with hooks — but Error Boundaries are the exception. They require two lifecycle methods that only exist on class components.
This is one of the very few situations where you still need to write a class component in 2024. You won't do it often, but you need to understand why it works this way.
A class component becomes an Error Boundary by implementing one or both of these methods:
static getDerivedStateFromError(error) — called when a child throws. Use it to update state so the next render shows fallback UI. This must be a pure function — no side effects.componentDidCatch(error, info) — called after the error is caught. Use it for side effects like logging the error to a reporting service.To implement an Error Boundary component, you need to provide static getDerivedStateFromError which lets you update state in response to an error and display an error message to the user. You can also optionally implement componentDidCatch to add some extra logic, for example, to log the error to an analytics service.
Here's the standard implementation, taken directly from the React docs:
import * as React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render shows the fallback UI
return { hasError: true };
}
componentDidCatch(error, info) {
// Log the error to an error reporting service
// info.componentStack tells you which component threw
console.error('ErrorBoundary caught:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
// Render any fallback UI — this.props.fallback lets callers customise it
return this.props.fallback;
}
return this.props.children;
}
}
Using it looks exactly like you'd expect:
<ErrorBoundary fallback={<p>Something went wrong loading this section.</p>}>
<ConcertList />
</ErrorBoundary>
If ConcertList or anything inside it throws during rendering, the boundary catches it and renders the <p> instead.
<aside> 💡
Info
You'll notice this is the first class component you've written in this course. Don't worry — you write it once, drop it in your src/components/ folder, and reuse it everywhere. You don't need to deeply understand class component syntax to use Error Boundaries effectively. The React docs also recommend the react-error-boundary package as an alternative, which wraps this class component for you and adds useful extras like a reset button.
</aside>
This is the most important section to read carefully. Error Boundaries are powerful, but they only intercept a specific category of errors.
Error boundaries do not catch errors for: event handlers, server side rendering, errors thrown in the error boundary itself (rather than its children), or asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks).
In plain terms:
Error Boundaries catch:
Error Boundaries do NOT catch:
onClick, onChange, or other event handlersuseEffect or async functionssetTimeout / setInterval callbacksThe reason event handlers are excluded is worth understanding. React doesn't need error boundaries to recover from errors in event handlers. Unlike the render method and lifecycle methods, the event handlers don't happen during rendering. So if they throw, React still knows what to display on the screen. For event handler errors, a regular try/catch block is the right tool.
// ✅ Handle errors in event handlers with try/catch
async function handleSaveConcert() {
try {
await saveConcert(formData);
} catch (error) {
setErrorMessage(error.message); // local state for UI feedback
}
}
// ❌ This error will NOT be caught by an Error Boundary above it
function ConcertCard({ concert }) {
return (
<button onClick={() => {
throw new Error('This will not be caught by an Error Boundary');
}}>
Click me
</button>
);
}
<aside> ⚠️
Warning
A common mistake is wrapping an async data fetch in an Error Boundary and expecting it to catch network errors. It won't — the fetch happens outside React's rendering cycle. Handle async errors with local state (useState for an error value), or use the useErrorBoundary hook from the react-error-boundary package, which lets you manually trigger the nearest boundary from async code.
</aside>
Where you place your Error Boundaries matters a lot. Think of them like try/catch blocks in regular JavaScript — you don't wrap your entire program in one giant try/catch. You place them at meaningful boundaries where you can handle the error in a useful way.
The granularity of error boundaries is up to you. You may wrap top-level route components to display a "Something went wrong" message to the user, just like how server-side frameworks often handle crashes. You may also wrap individual widgets in an error boundary to protect them from crashing the rest of the application.
A practical example: your concert app has a sidebar showing related artists, a main concert details panel, and a ticket purchasing widget. These are good candidates for separate Error Boundaries:
function ConcertPage({ concertId }) {
return (
<div className="concert-page">
{/* If the sidebar crashes, the main content still works */}
<ErrorBoundary fallback={<SidebarError />}>
<RelatedArtistsSidebar concertId={concertId} />
</ErrorBoundary>
{/* Main content has its own boundary */}
<ErrorBoundary fallback={<ConcertDetailError />}>
<ConcertDetails concertId={concertId} />
</ErrorBoundary>
{/* Ticket purchasing is isolated — a crash here doesn't kill the page */}
<ErrorBoundary fallback={<TicketPurchaseError />}>
<TicketPurchaseWidget concertId={concertId} />
</ErrorBoundary>
</div>
);
}
For example, Facebook Messenger wraps content of the sidebar, the info panel, the conversation log, and the message input into separate error boundaries. If some component in one of these UI areas crashes, the rest of them remain interactive.
Contrast this with the "wrap everything in one boundary" approach:
// ⚠️ Too coarse — one crash anywhere takes down the whole page
function App() {
return (
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<EntireApplication />
</ErrorBoundary>
);
}
This is better than nothing, but it gives users no context about what failed and no way to continue using the parts of the app that still work. A top-level boundary is a good final safety net, but it shouldn't be your only one.
<aside> ⌨️
Hands On
Create a component called BrokenConcertCard that immediately throws an error when rendered: throw new Error('Failed to load concert'). Render it alongside a working ConcertCard component. First, observe what happens with no Error Boundary — the whole page goes blank. Then wrap only BrokenConcertCard in an Error Boundary with a fallback message. Confirm that the working ConcertCard still renders correctly.
</aside>
The class component above is fine, but a more flexible version accepts a customisable fallback prop and resets when its key changes — a pattern the React docs recommend for recovering from errors:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// In a real app, send this to Sentry, Datadog, etc.
console.error('Caught by ErrorBoundary:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
// If a FallbackComponent prop is provided, render it with error details
if (this.props.FallbackComponent) {
return (
<this.props.FallbackComponent
error={this.state.error}
resetErrorBoundary={() => this.setState({ hasError: false, error: null })}
/>
);
}
// Otherwise fall back to the simpler fallback prop
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
// A reusable fallback component for the concert app
function ConcertSectionError({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>This section failed to load.</p>
<p><small>{error.message}</small></p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Usage
<ErrorBoundary FallbackComponent={ConcertSectionError}>
<ConcertDetails concertId={id} />
</ErrorBoundary>
<aside> 💡
Info
The role="alert" on the fallback div is important for accessibility. Screen readers will announce the content of an alert role automatically when it appears, so users relying on assistive technology will know something went wrong — not just sighted users reading the fallback UI.
</aside>
<aside> ⌨️
Hands On
Extend the ErrorBoundary from above to accept a FallbackComponent prop. Create a ConcertLoadError component that receives error and resetErrorBoundary as props and renders a user-friendly message with a "Try again" button. Wire them together around a component that throws. Click the "Try again" button and observe that it clears the error state and attempts to render the children again.
</aside>
There is currently no way to write an Error Boundary as a function component. However, you don't have to write the Error Boundary class yourself. You can use react-error-boundary instead.
The react-error-boundary package — maintained by a React core team member — gives you a pre-built ErrorBoundary component with a clean API, plus a useErrorBoundary hook for surfacing async errors to the nearest boundary. Most real-world React projects use this rather than rolling their own class component:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function ConcertErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Failed to load concert data.</p>
<p><small>{error.message}</small></p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function ConcertPage() {
return (
<ErrorBoundary
FallbackComponent={ConcertErrorFallback}
onReset={() => {
// Optionally reset any state that caused the crash
}}
onError={(error, info) => {
// Log to your error tracking service
console.error(error, info.componentStack);
}}
>
<ConcertDetails />
</ErrorBoundary>
);
}
The useErrorBoundary hook lets you manually trigger the nearest boundary from inside an async operation — bridging the gap that the standard API leaves open:
import { useErrorBoundary } from 'react-error-boundary';
function ConcertDetails({ concertId }) {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
async function loadConcert() {
try {
const data = await fetchConcert(concertId);
setConcert(data);
} catch (error) {
// Manually send the error to the nearest Error Boundary
showBoundary(error);
}
}
loadConcert();
}, [concertId]);
// ...
}
<aside> ⌨️
Hands On
Install react-error-boundary and replace your hand-rolled class component with the library's ErrorBoundary. Keep the same ConcertLoadError fallback component — only the boundary wrapper changes. Then add a useErrorBoundary hook inside a component that simulates a failed fetch (use Promise.reject(new Error('Network error')) inside a useEffect). Confirm the fallback renders correctly for both render-time and async errors.
</aside>
ErrorBoundary once and reuse it. Place it in src/components/ErrorBoundary.jsx and import it wherever you need it. Don't copy-paste the class component across files.componentDidCatch for error reporting. In production, log errors to a service like Sentry or Datadog. Silent failures are hard to debug.react-error-boundary in real projects. The hand-rolled class component is valuable to understand, but the library saves boilerplate and handles edge cases you'd otherwise miss.<aside> 🎉
Celebration
Error Boundaries are one of those features that feels small until the day a component crashes in production and your users see a fallback message instead of a blank screen. You now know what they catch, what they don't, how to write one, and — critically — where to put them. That placement instinct is what separates a resilient UI from a fragile one.
</aside>
Videos
Reading