Welcome back, future React maestro! So far, we’ve learned how to build components, manage state, and fetch data. Our applications are starting to look quite impressive! But what happens when things go wrong? And trust me, in the world of software, things will go wrong. A network request might fail, a prop might be undefined, or a wild bug might appear.
In this chapter, we’re going to tackle a crucial aspect of building production-ready applications: robust error handling. We’ll learn how to gracefully handle unexpected errors in our React components, prevent our entire application from crashing, and provide a much better experience for our users. Instead of a blank screen or a cryptic error message, we’ll learn to show a friendly fallback UI and log the issue for debugging.
By the end of this chapter, you’ll understand:
- Why traditional
try...catchblocks aren’t enough for React rendering errors. - What React Error Boundaries are and how they work.
- How to implement your own Error Boundary component.
- Best practices for integrating error boundaries into your application.
- How to leverage modern libraries for simpler error handling in functional components.
Ready to make your apps unbreakable (or at least gracefully breakable)? Let’s dive in!
The Problem with try...catch in React
You’re probably familiar with try...catch blocks from JavaScript. They’re excellent for handling synchronous errors in your code:
try {
// Code that might throw an error
let result = someFunctionThatMightFail();
console.log(result);
} catch (error) {
// Handle the error
console.error("An error occurred:", error);
}
This works perfectly for event handlers, asynchronous operations (like fetch calls), and other imperative code within your React components.
However, try...catch does not work for errors that occur during the rendering phase of a React component. This includes:
- Errors in component constructors.
- Errors in render methods (
renderfor class components, or the function body for functional components). - Errors in lifecycle methods (like
componentDidMount,componentDidUpdate). - Errors in any children components’ constructors, render methods, or lifecycle methods.
Why? Because React’s rendering process is asynchronous and declarative. When an error occurs during rendering, it bubbles up the component tree. If not caught, it will “unmount” the entire React component tree, leaving your users with a broken, blank page. Not ideal, right?
This is where React Error Boundaries come to the rescue!
Introducing React Error Boundaries
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 an Error Boundary like a safety net. You wrap a section of your application with it, and if any component inside that section throws an error during rendering, the Error Boundary catches it, preventing the entire application from collapsing.
How Do Error Boundaries Work?
Error Boundaries are special class components that implement one or both of two lifecycle methods:
static getDerivedStateFromError(error): This static method is called after an error has been thrown by a descendant component. It receives the error that was thrown and should return an object to update the boundary’s state. This state update then triggers a re-render of the boundary component, allowing it to display a fallback UI.componentDidCatch(error, errorInfo): This method is also called after an error has been thrown by a descendant component. It receives the error and an object with information about which component threw the error (componentStack). This is where you’d typically perform side effects, like logging the error to an error reporting service.
Let’s visualize this flow:
In this diagram:
Problematic Componentthrows an error during rendering.- The error is caught by the
Error Boundary. getDerivedStateFromErrorupdates theError Boundary’s state to indicate an error, triggering a re-render.componentDidCatchis used to log the error to an external service.- The
Error Boundarythen renders itsFallback UIinstead of its children.
Important Considerations:
- Error Boundaries only catch errors in their children. An Error Boundary cannot catch errors within itself. If its
rendermethod or lifecycle methods throw an error, it won’t catch it. - They catch errors in the render phase,
componentDidMount,componentDidUpdate,componentWillUnmount(for class components), and constructors of children. - They do not catch errors in:
- Event handlers (use
try...catchhere). - Asynchronous code (
setTimeout,requestAnimationFrame,fetch/Promisecallbacks - usetry...catchhere). - Server-side rendering (SSR) errors.
- Errors thrown in the Error Boundary itself.
- Event handlers (use
Step-by-Step Implementation: Building Our Own Error Boundary
Let’s create a reusable ErrorBoundary component and then demonstrate how it catches errors.
Step 1: Create the ErrorBoundary Component
We’ll start by creating a new file, src/components/ErrorBoundary.jsx.
// src/components/ErrorBoundary.jsx
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
// Initialize state to track if an error has occurred
this.state = { hasError: false, error: null, errorInfo: null };
}
// This static method is called after an error is thrown by a descendant component.
// It receives the error that was thrown and should return an object to update state.
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
console.log("ErrorBoundary: getDerivedStateFromError called");
return { hasError: true, error: error };
}
// This method is called after an error has been thrown by a descendant component.
// It's ideal for logging the error information.
componentDidCatch(error, errorInfo) {
console.log("ErrorBoundary: componentDidCatch called");
// You can also log error messages to an error reporting service here
// For example: Sentry.captureException(error, { extra: errorInfo });
console.error("Uncaught error:", error, errorInfo);
this.setState({ errorInfo: errorInfo });
}
render() {
// If an error occurred, render the fallback UI
if (this.state.hasError) {
return (
<div style={{ padding: '20px', border: '1px solid red', borderRadius: '5px', backgroundColor: '#ffe6e6' }}>
<h2>Oops! Something went wrong.</h2>
<p>We're sorry for the inconvenience. Please try refreshing the page.</p>
{/* Optionally, display error details in development for debugging */}
{process.env.NODE_ENV === 'development' && this.state.error && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
// Otherwise, render the children components as normal
return this.props.children;
}
}
export default ErrorBoundary;
Explanation:
- We define a
class ErrorBoundary extends React.Component. Remember, Error Boundaries must be class components. - In the
constructor, we initializethis.statewithhasError: false. This flag will tell us whether an error has occurred. static getDerivedStateFromError(error): This is the critical method that detects an error. When a child component throws, this method is called, and we sethasErrortotrueand store theerrorobject. React then re-renders theErrorBoundarywith this new state.componentDidCatch(error, errorInfo): This method is called aftergetDerivedStateFromError. It’s primarily for side effects, like sending the error details to a logging service (e.g., Sentry, Bugsnag, or a custom backend endpoint). We alsoconsole.errorit for development visibility.- In the
render()method, we checkthis.state.hasError.- If
true, we render our custom fallback UI (<h2>Oops! Something went wrong.</h2>). We also conditionally show more details indevelopmentmode usingprocess.env.NODE_ENV. - If
false, we simply renderthis.props.children, meaning the components wrapped by theErrorBoundarywill render as usual.
- If
Step 2: Create a Component That Throws an Error
Now, let’s create a component that’s designed to break. We’ll call it src/components/BuggyCounter.jsx.
// src/components/BuggyCounter.jsx
import React, { useState } from 'react';
function BuggyCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1);
};
// This will throw an error when count reaches 5
if (count === 5) {
throw new Error('I crashed! (This is an intentional error)');
}
return (
<div style={{ border: '1px dashed gray', padding: '10px', margin: '10px' }}>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
<p>Click the button until the count reaches 5 to see the error.</p>
</div>
);
}
export default BuggyCounter;
Explanation:
- This is a simple functional component with a counter.
- The critical part is the
if (count === 5) { throw new Error(...) }. This line will intentionally cause an error during theBuggyCounter’s render phase whencountbecomes 5.
Step 3: Integrate the Error Boundary
Let’s use our ErrorBoundary to protect our BuggyCounter. Open src/App.jsx and modify it.
// src/App.jsx
import React from 'react';
import ErrorBoundary from './components/ErrorBoundary'; // Import our ErrorBoundary
import BuggyCounter from './components/BuggyCounter'; // Import the BuggyCounter
function App() {
return (
<div style={{ fontFamily: 'Arial, sans-serif', textAlign: 'center', padding: '20px' }}>
<h1>React Error Handling Demo</h1>
<h2>Without Error Boundary (will crash the app)</h2>
<p>
If you uncomment the `BuggyCounter` below, clicking it until count is 5 will crash the entire application.
Refresh to reset.
</p>
{/* <BuggyCounter /> Uncomment this to see the app crash without protection! */}
<hr style={{ margin: '40px 0' }} />
<h2>With Error Boundary (graceful fallback)</h2>
<p>
This `BuggyCounter` is wrapped in an Error Boundary. When it crashes, only the
Error Boundary's fallback UI will show, and the rest of the app remains functional.
</p>
<ErrorBoundary>
<BuggyCounter />
</ErrorBoundary>
<hr style={{ margin: '40px 0' }} />
<h2>Multiple Error Boundaries (isolated failures)</h2>
<p>
You can wrap different parts of your UI with separate Error Boundaries to isolate failures.
</p>
<div style={{ display: 'flex', justifyContent: 'space-around', gap: '20px' }}>
<ErrorBoundary>
<p>Counter 1 (protected)</p>
<BuggyCounter />
</ErrorBoundary>
<ErrorBoundary>
<p>Counter 2 (protected)</p>
<BuggyCounter />
</ErrorBoundary>
</div>
<hr style={{ margin: '40px 0' }} />
<p>This part of the app is outside any Error Boundary and will always render.</p>
</div>
);
}
export default App;
Run your application (e.g., npm start or yarn start).
Experiment:
- Click the “Increment” button for the
BuggyCounterwrapped in anErrorBoundary. When the count reaches 5, you’ll see the “Oops! Something went wrong.” message, but the rest of yourAppcomponent will remain visible and interactive. Check your browser’s console for the logged error! - Try uncommenting the
BuggyCounterwithout anErrorBoundary. Click it until it reaches 5, and you’ll see the entire application crash, likely leaving a blank screen or a React error overlay (in development mode). This highlights the importance of Error Boundaries.
Step 4: Enhancing the Error Boundary (Logging)
Our componentDidCatch already logs to the console, but in a real application, you’d integrate with an external service. While we won’t set up a full service here, understand that this is where you’d send error and errorInfo to tools like:
- Sentry: A leading error tracking and performance monitoring platform.
- Bugsnag: Another popular error monitoring solution.
- Datadog, New Relic: Broader observability platforms that include error tracking.
- Your own custom backend API for logging.
// src/components/ErrorBoundary.jsx (Update componentDidCatch)
// ... (previous code)
componentDidCatch(error, errorInfo) {
console.log("ErrorBoundary: componentDidCatch called");
console.error("Uncaught error:", error, errorInfo);
// --- Start of enhanced logging ---
// In a real application, you'd send this to an error reporting service.
// Example with a hypothetical logging service:
// logService.reportError(error, {
// componentStack: errorInfo.componentStack,
// location: this.props.location // If you pass location as a prop
// });
// For now, let's simulate sending it.
console.log("Simulating sending error to a reporting service:");
console.log({
errorMessage: error.message,
errorStack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
// Add any other relevant context, like user ID, app version, etc.
appVersion: '1.0.0',
environment: process.env.NODE_ENV,
});
// --- End of enhanced logging ---
this.setState({ errorInfo: errorInfo });
}
// ... (rest of the component)
Step 5: Using a Library for Functional Components (Modern Best Practice)
While you must use a class component to create an Error Boundary itself, if you want to use Error Boundaries within a functional component more easily, or simply prefer a more declarative API, libraries can help.
A popular and well-maintained library is react-error-boundary. As of January 2026, it’s still a widely recommended solution.
Installation:
First, install the library:
npm install react-error-boundary@^4.0.0
# or
yarn add react-error-boundary@^4.0.0
(Note: ^4.0.0 ensures compatibility with the latest major version as of 2026-01-31, which is 4.0.13 at time of writing).
Let’s modify src/App.jsx to use this library’s ErrorBoundary and its useErrorBoundary hook.
// src/App.jsx
import React from 'react';
// import our custom ErrorBoundary from './components/ErrorBoundary'; // No longer strictly needed for this example
import { ErrorBoundary } from 'react-error-boundary'; // Import from the library
import BuggyCounter from './components/BuggyCounter';
import { useState } from 'react';
// A simple fallback component for react-error-boundary
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" style={{ padding: '20px', border: '1px solid orange', borderRadius: '5px', backgroundColor: '#fffbe6' }}>
<p>Something went wrong (from library fallback):</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Another component that can throw an error, but this time with a button
// to intentionally trigger the error boundary's reset functionality.
function ProblematicComponentWithReset() {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
throw new Error('I crashed from ProblematicComponentWithReset!');
}
return (
<div style={{ border: '1px dashed blue', padding: '10px', margin: '10px' }}>
<p>This component can throw an error.</p>
<button onClick={() => setShouldThrow(true)}>
Trigger Error (and allow reset)
</button>
</div>
);
}
function App() {
const logError = (error, info) => {
// Do something with the error, e.g. log to an external API
console.log("Error caught by react-error-boundary:", error, info);
};
return (
<div style={{ fontFamily: 'Arial, sans-serif', textAlign: 'center', padding: '20px' }}>
<h1>React Error Handling Demo (with react-error-boundary)</h1>
<hr style={{ margin: '40px 0' }} />
<h2>Using `react-error-boundary`</h2>
<p>
This library provides a convenient `ErrorBoundary` component and a `useErrorBoundary` hook.
It's great for functional components and offers a `resetKeys` prop for automatic reset.
</p>
<ErrorBoundary
FallbackComponent={ErrorFallback} // Pass a component for the fallback UI
onError={logError} // Callback for when an error is caught
// resetKeys={[someKey]} // Optional: if these keys change, the boundary resets its error state
>
<BuggyCounter />
</ErrorBoundary>
<hr style={{ margin: '40px 0' }} />
<h2>`react-error-boundary` with Reset Functionality</h2>
<p>
The `resetErrorBoundary` function passed to the `FallbackComponent` allows users to try
re-rendering the children after an error, which can be useful if the error was transient.
</p>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logError}
// onReset={() => console.log("ErrorBoundary reset!")} // Optional: callback when reset is called
>
<ProblematicComponentWithReset />
</ErrorBoundary>
<hr style={{ margin: '40px 0' }} />
<p>This part of the app is outside any Error Boundary and will always render.</p>
</div>
);
}
export default App;
Explanation:
- We import
ErrorBoundaryfrom'react-error-boundary'. - We define
ErrorFallback, a functional component that receiveserrorandresetErrorBoundaryas props. This component is rendered when an error occurs. - The
ErrorBoundarycomponent from the library accepts:FallbackComponent: A component to render when an error occurs.onError: A callback function that gets called when an error is caught, similar tocomponentDidCatch.resetErrorBoundary: A function provided toFallbackComponentthat, when called, clears the error state and re-renders the children. This is incredibly useful for transient errors or allowing users to try again.
ProblematicComponentWithResetdemonstrates how to use theresetErrorBoundaryfunctionality.
This library simplifies the boilerplate of creating Error Boundaries and offers more declarative ways to handle them, especially in a world dominated by functional components and hooks.
Mini-Challenge: Isolate a Network Error
Let’s put your knowledge to the test!
Challenge:
- Create a new functional component called
UserProfileDisplay. - Inside
UserProfileDisplay, simulate fetching user data. Make this fetch operation sometimes fail (e.g., throw an error if auserIdprop isnullorundefined, or if a random number falls below a threshold). - Wrap
UserProfileDisplayin thereact-error-boundary’sErrorBoundaryinApp.jsx. - Ensure that when
UserProfileDisplayfails, only its section shows the fallback UI, and the rest of the application remains functional. - Make sure your
onErrorcallback logs the specific error message from your simulated fetch failure.
Hint: You can use useEffect to simulate an async fetch and useState to manage loading/error states within UserProfileDisplay. Throwing an error directly in useEffect can be tricky, but an error in a Promise.reject() or during state updates after an async operation can be caught by the boundary.
What to observe/learn:
- How to integrate Error Boundaries around components with asynchronous operations.
- The difference between errors caught by the boundary and errors that crash the app.
- How to provide specific error messages in your fallback UI based on the error.
Common Pitfalls & Troubleshooting
- Error Boundaries Don’t Catch Errors in Themselves: If your
ErrorBoundary’srendermethod orgetDerivedStateFromError/componentDidCatchmethods throw an error, it cannot catch its own error. This will still crash your application. Keep your Error Boundary component as simple and robust as possible. - Not Catching Event Handler Errors: Remember, Error Boundaries are primarily for rendering-related errors. Errors in event handlers (e.g.,
onClick,onChange) are not caught by Error Boundaries. For these, use traditionaltry...catchblocks within the event handler function.function MyComponent() { const handleClick = () => { try { // This code will be caught by try...catch, not an Error Boundary throw new Error("Error in event handler!"); } catch (e) { console.error("Caught in event handler:", e); } }; return <button onClick={handleClick}>Click Me</button>; } - Over-scoping vs. Under-scoping:
- Over-scoping: Wrapping your entire
Appcomponent with oneErrorBoundarymight catch all errors, but it means a small bug in one widget could take down the whole page with a generic error message. - Under-scoping: Not using enough Error Boundaries, leaving large sections of your UI unprotected.
- Best Practice: Place Error Boundaries strategically around logical blocks of your UI (e.g., around a complex data table, a user profile section, a third-party widget). This isolates failures and allows you to provide more specific fallback UIs.
- Over-scoping: Wrapping your entire
- Not Logging Errors: Just displaying a fallback UI isn’t enough. Always log the error details to a reporting service so you can diagnose and fix the problem.
componentDidCatch(or theonErrorprop inreact-error-boundary) is your best friend here. - Forgetting
keyProp with Lists: If you’re rendering a list of components, and one of them errors out, a single Error Boundary around the entire list might render a fallback for the whole list. If you want to gracefully handle errors for individual items, each item needs its own Error Boundary, and they must have uniquekeyprops.
Summary
Congratulations! You’ve taken a significant step towards building more resilient and user-friendly React applications.
Here’s what we covered:
try...catchlimitations: We learned that traditionaltry...catchdoesn’t protect against errors during React’s rendering phase.- Error Boundaries Defined: These are special React class components that catch errors in their child component tree.
- Lifecycle Methods: We explored
static getDerivedStateFromError(for updating state to show fallback UI) andcomponentDidCatch(for logging errors). - Implementation: We built our own
ErrorBoundarycomponent from scratch and demonstrated how to use it to protect other components. - Modern Approach: We saw how libraries like
react-error-boundarysimplify error boundary implementation, especially for functional components, and provide useful features like reset functionality. - Best Practices: We discussed strategic placement of boundaries, the importance of logging, and common pitfalls to avoid.
By thoughtfully applying Error Boundaries, you can prevent your entire application from crashing, provide a better experience for your users, and gain valuable insights into production issues.
In the next chapter, we’ll delve into Suspense and Concurrent Features, which often go hand-in-hand with robust loading and error states, allowing you to manage asynchronous UI patterns with even greater elegance!
References
- React Official Documentation - Error Boundaries: The definitive guide to Error Boundaries.
- https://react.dev/learn/managing-state#reacting-to-input-with-state (This is the general ‘Learn’ section, search for ‘Error Boundaries’ within it or use the more specific link if available. As of 2026, react.dev is the primary source).
- https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
react-error-boundaryGitHub Repository: Official source for the popular library.- MDN Web Docs -
try...catch: For a refresher on general JavaScript error handling.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.