We’ve all been there. You ship a new feature, everything looks perfect on your machine, and then… the support tickets roll in. Users are seeing nothing. Just a blank white screen. That dreaded white screen of death. A single, uncaught JavaScript error in one tiny component has somehow managed to take down your entire application.
It’s a heart-stopping, cold-sweat moment for any developer.
For years, this was just a painful reality of building complex single-page apps. An error during rendering was catastrophic. But then, React gave us a superpower: Error Boundaries.
Honestly, they’re one of the most underrated features in the entire ecosystem. They are the seatbelts and airbags of your React app. You really hope you never need them, but when you do, they save you from a total wreck. Today, we’re going to build them, get to know them, and learn how to use them to make our apps practically bulletproof.
So, What is a React Error Boundary, Really?
Alright, let's get the official definition out of the way. According to the official React docs, an Error Boundary is a React component that catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of the component tree that crashed.
Think of it like a try...catch block, but for your components.
// This is what you're used to in regular JavaScript
try {
doSomethingRisky();
} catch (error) {
console.error("Oops, that didn't work.");
}
// This is the React equivalent for components
<ErrorBoundary>
<MyRiskyComponent />
</ErrorBoundary>
Suddenly, a single error inside MyRiskyComponent won't crash your entire app anymore. Instead, the ErrorBoundary will catch it and render something else—a friendly message, a "try again" button, anything but that terrifying blank page.
The Catch (There's Always a Catch)
Before we go any further, you need to know what Error Boundaries don’t do. They’re powerful, but they’re not magic. They do not catch errors in:
- Event handlers (like
onClickoronChange) - Asynchronous code (
setTimeout,requestAnimationFrame, promises) - Server-side rendering
- Errors thrown in the error boundary itself (talk about irony)
This isn’t a bug; it’s by design. React knows that by the time an onClick handler runs, it's not directly involved in rendering anymore. For those cases, you still need your good old try...catch blocks. We'll circle back to this and show some clever patterns later on.
For now, just remember: Error Boundaries are for errors that happen during rendering and in lifecycle methods.
Let's Build Our First Error Boundary
Okay, theory time is over. Let's get our hands dirty.
There’s one little quirk you need to know upfront: as of today, only class components can be error boundaries. This is because they need access to two special lifecycle methods that don't have functional Hook equivalents yet: getDerivedStateFromError() and componentDidCatch().
Don’t worry if you’re a die-hard hooks fan. You only have to write this class component once and then you can use it everywhere. Here's the most basic version possible:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
// Start with a clean slate. No error yet!
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// This is the catcher's mitt.
// When a child component throws an error, this method is called.
// It should return an object to update the state.
console.log("Caught an error!", error);
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// This is the reporter. It gets called after the error is caught.
// It's the perfect place to log the error to a service.
console.error("Uncaught error:", error, errorInfo);
// You could send this to Sentry, LogRocket, etc.
// logErrorToMyService(error, errorInfo);
}
render() {
// The conditional rendering magic happens here.
if (this.state.hasError) {
// If an error was caught, render our fallback UI.
return <h1>Oops! Something went wrong. Please refresh the page.</h1>;
}
// If everything is fine, just render the children.
return this.props.children;
}
}
export default ErrorBoundary;
Let's break down the two key players here:
static getDerivedStateFromError(error): This is step one. React calls this method during the "render" phase whenever a descendant component throws an error. Its only job is to update state, which tells the component to show the fallback UI on the next pass. You should not cause any side effects here (like logging).componentDidCatch(error, errorInfo): This is step two. It gets called after the error has been caught and the fallback UI is about to be committed to the DOM. This is where you do your side effects, like logging. ThaterrorInfoobject it receives is super useful—it contains acomponentStackwhich shows you exactly where in the tree the error happened. It's like a black box recorder for your app.
Making Our Error Boundary Actually Useful
The basic version works, but let's be real—<h1>Oops!</h1> isn't very helpful. A good fallback UI should do three things: inform the user, give them a way to recover, and (for us developers) provide some handy debug info during development.
Let's build a better one.
import React from 'react';
class AdvancedErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// We'll also store the error details in state to display them
this.setState({
error: error,
errorInfo: errorInfo
});
// And of course, log it to our error reporting service
// logErrorToService(error, errorInfo);
console.error("Error logged from componentDidCatch:", error, errorInfo);
}
// A simple reset function
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '2rem', border: '2px dashed red', margin: '1rem' }}>
<h2>Something went wrong.</h2>
<p>We're sorry for the inconvenience. Please try again.</p>
<button onClick={this.handleReset}>
Try Again
</button>
{/* Super useful for debugging! */}
{process.env.NODE_ENV === 'development' && (
<details style={{ marginTop: '1rem', whiteSpace: 'pre-wrap' }}>
<summary>Click for error details</summary>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
return this.props.children;
}
}
export default AdvancedErrorBoundary;
Now that's much better. We have a "Try Again" button that resets the error state, potentially letting the user recover without a full page refresh. And that little process.env.NODE_ENV check is an absolute lifesaver in development.
Where to Place Error Boundaries: The Art of Containment
My first instinct when I learned about this was to just wrap my entire <App /> component in a single ErrorBoundary. Done. Problem solved, right?
Well, that's a bit of a rookie mistake. While it's definitely better than nothing, it's like using a fire extinguisher to put out a match—total overkill. If a tiny button in your sidebar has an error, do you really want to replace the entire application with an error message? Probably not.
The real power of React Error Boundaries comes from strategic placement. Think of them like the bulkheads in a ship. If one section gets a hole, you seal it off, and the rest of the ship stays afloat.
Here’s a much more sensible layout:
function App() {
return (
<div>
<Navbar />
<ErrorBoundary fallback={<p>⚠️ Our sidebar is having some issues.</p>}>
<Sidebar />
</ErrorBoundary>
<main>
<ErrorBoundary fallback={<h2>Could not load user profile.</h2>}>
<UserProfileWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<h2>Could not load feed.</h2>}>
<NewsFeed />
</ErrorBoundary>
</main>
<Footer />
</div>
);
}
See the difference? Now, an error in the Sidebar only takes down the sidebar. The user can still see and interact with the NewsFeed and the rest of the app. This is called graceful degradation, and it’s a hallmark of a professional, resilient user interface.
The Modern Way: react-error-boundary
Okay, I hear you. "A class component? In this economy?"
I get it. That's why the brilliant Kent C. Dodds created the react-error-boundary library. It's a tiny, production-ready package that gives you all the power of error boundaries with a beautiful, modern, props-based API. I use it in almost every project I touch.
First, you'll need to install it:
npm install react-error-boundary
Now, just look at how clean this is:
import { ErrorBoundary } from 'react-error-boundary';
// Your fallback component gets props with the error and a reset function!
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function MyApp() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// You can reset application state here, e.g., clear a cache
console.log('Boundary has been reset!');
}}
onError={(error, info) => {
// This is your componentDidCatch
console.error("Logged from react-error-boundary:", error, info);
}}
>
<ComponentThatMightCrash />
</ErrorBoundary>
);
}
Isn't that nice? Not a class component in sight. You just provide a FallbackComponent, and the library handles all the state management and lifecycle logic behind the scenes. It even has advanced features like resetKeys to automatically reset the boundary when some data changes. For more complex scenarios, check out our guide on building custom React hooks.
Handling the Errors That Get Away
Remember our list of things error boundaries don't catch? Let's figure out how to tackle them.
1. Asynchronous Code (Promises, useEffect)
This is probably the most common "gotcha." An error inside a .then() or an async/await block happens outside of the React render cycle, so the boundary is completely blind to it.
The trick is to catch the error ourselves and then use state to "re-throw" it during the next render. I know it sounds a little weird, but it works perfectly.
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/invalid-url');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
// Catch the async error and put it in state
setError(err);
}
};
fetchData();
}, []);
// On the next render, if there's an error in state, throw it!
// Now the Error Boundary can see it and catch it.
if (error) {
throw error;
}
return <div>{data ? `Data loaded!` : 'Loading...'}</div>;
}
2. Event Handlers
This one is much simpler. Since event handlers are just standard JavaScript functions, our good old try...catch block is your best friend.
function RiskyButton() {
const handleClick = () => {
try {
// Some function that might fail
potentiallyCrashingFunction();
} catch (error) {
console.error("Failed on click:", error);
// You could update state here to show an inline error message
// or report to a logging service.
}
};
return <button onClick={handleClick}>Click Me</button>;
}
Frequently Asked Questions
Q: Can I write an Error Boundary as a function component with Hooks?
A: Not yet. I know, I know. The lifecycle methods
getDerivedStateFromErrorandcomponentDidCatchthat are required to implement an error boundary are currently only available for class components. For now, the best practice is to either use a wonderful library likereact-error-boundaryor just write one class component and reuse it everywhere.
Q: What's the real difference between
getDerivedStateFromErrorandcomponentDidCatch?A: It helps to think of it as a two-step process.
getDerivedStateFromErroris called first, during the "render" phase. Its only job is to update state so that a fallback UI can be shown. It's meant to be a pure function. Then,componentDidCatchis called after that, during the "commit" phase. This one is for side effects, like logging the error to an external service.
Q: How many error boundaries should I use in my app?
A: Ah, the classic "it depends!" There's no magic number. A good rule of thumb is to wrap any distinct "widget" or section of your UI that can fail independently and isn't critical to the core function of the rest of the page. A sidebar, a chat widget, a data grid—these are all great candidates. Don't go overboard, but definitely don't just use one at the very top level either.
The Final Word
Building for the web means building for failure. Things will go wrong. APIs will fail, unexpected data will arrive, and yes, bugs will slip through. React Error Boundaries are your first and best line of defense.
They transform a catastrophic application crash into a manageable, contained hiccup. Using them is a sign of a mature developer who cares not just about the "happy path," but about creating a robust and professional experience for the user, even when things go sideways.
So go ahead, wrap those components. Give your users the resilient experience they deserve. And give yourself the peace of mind that comes from knowing your app won't fall apart at the first sign of trouble.