Let’s face it: software breaks. APIs timeout, undefined is not a function, and third-party widgets throw tantrums. In the world of Single Page Applications (SPAs), a single unhandled JavaScript error in a nested component can bring down your entire application, resulting in the dreaded “White Screen of Death.”
In 2025, with React 19 pushing the boundaries of Server Components and concurrent rendering, robust error handling isn’t just a “nice-to-have”—it’s a critical architectural requirement. If you aren’t handling errors gracefully, you are bleeding users.
This guide dives into the current state of Error Boundaries in React 19. We will move beyond the documentation basics to look at production patterns, handling async errors, and why you (unfortunately) still might need a Class Component.
Prerequisites #
Before we start crashing components on purpose, ensure you have the following environment set up:
- Node.js: v20.x or higher (LTS recommended).
- React: v19.0+.
- Package Manager:
npm,yarn, orpnpm. - IDE: VS Code (or your weapon of choice).
The State of Error Boundaries in React 19 #
Here is the harsh truth: As of React 19, there is still no native Hook for creating an Error Boundary.
While we have moved to functional components for everything else, Error Boundaries are the last stronghold of the Class Component. A component is only an Error Boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch().
However, the ecosystem has adapted. Most senior developers rarely write these classes manually anymore, opting for robust wrappers. But to understand the wrapper, you must understand the core.
The Error Lifecycle #
Before writing code, let’s visualize how an error travels through a React tree.
1. The “Vanilla” Approach (The Class Component) #
Even if you plan to use a library later, you need this in your codebase for specific custom implementations. Here is a production-ready template that handles state updates and error logging.
Create components/ErrorBoundary.jsx:
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) {
// You can also log the error to an error reporting service
console.error("Uncaught error:", error, errorInfo);
// Example: logToSentry(error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
// Invoke parent reset callback if provided
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-xl font-bold text-red-800">Something went wrong.</h2>
<details className="mt-2 text-sm text-red-600">
<summary>Error Details</summary>
{this.state.error && this.state.error.toString()}
</details>
<button
onClick={this.handleReset}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;Why two lifecycle methods? #
getDerivedStateFromError: Called during the “render” phase. Use this to update state. Do not perform side effects here.componentDidCatch: Called during the “commit” phase. This is where you log errors to Sentry, LogRocket, or your console.
2. The Modern Approach: react-error-boundary
#
In 2025, writing class components feels archaic. The community standard is the react-error-boundary library. It wraps the class logic and exposes a clean API compatible with functional components and hooks.
First, add the dependency:
npm install react-error-boundary
# or
yarn add react-error-boundaryImplementing a Feature-Level Boundary #
Here is how we use it in a real-world scenario, such as a Dashboard widget that might fail without crashing the sidebar or header.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// A fallback component that receives the error and a reset function
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-container">
<p>Something went wrong with this widget:</p>
<pre className="text-red-500">{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function UserProfileWidget({ userId }) {
// Imagine this hook throws an error if 404 or Network Fail
const user = useUserData(userId);
return (
<div className="card">
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
export default function Dashboard() {
return (
<div className="dashboard-grid">
{/* Isolate the failure to just this component */}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset the state of your app so the error doesn't happen again
console.log("Resetting state...");
}}
>
<Suspense fallback={<div>Loading User...</div>}>
<UserProfileWidget userId="123" />
</Suspense>
</ErrorBoundary>
</div>
);
}3. Granularity Strategy: Where to place them? #
One of the most common mistakes is wrapping the entire <App /> in a single boundary. While this prevents a white screen, it provides a terrible UX—if a footer link fails, the user loses access to the main navigation.
Here is a comparison of placement strategies:
| Strategy | Scope | Pros | Cons |
|---|---|---|---|
| Global Wrap | Root (index.jsx) |
Prevents total crash (WSOD). Easy to setup. | One bug kills the whole app experience. |
| Page Wrap | Route Level (React Router) | Isolates errors to specific pages. Navigation remains functional. | Still loses the entire page context. |
| Feature Wrap | Specific Widgets/Sections | Best UX. If the “Comments” section crashes, the “Video Player” still works. | Requires more boilerplate code. |
| Strict Wrap | Individual Components | Maximum isolation. | Can create visual clutter if many small components fail simultaneously. |
Best Practice: Use a Global Boundary for catastrophic failures (500s, auth issues), and Feature Boundaries for independent widgets (charts, feeds, comments).
4. The “Async” Trap and useErrorBoundary
#
Standard Error Boundaries catch errors during rendering, lifecycle methods, and constructors.
They do NOT catch errors in:
- Event handlers (e.g.,
onClick). - Asynchronous code (e.g.,
setTimeoutorrequestAnimationFramecallbacks). - Server Side Rendering (SSR) errors (on the server).
If you have an async fetch in a useEffect that fails, your Error Boundary will ignore it unless you explicitly throw it into the render cycle.
The react-error-boundary library provides a hook for this: useErrorBoundary.
import { useErrorBoundary } from 'react-error-boundary';
function AsyncComponent() {
const { showBoundary } = useErrorBoundary();
const handleAsyncAction = async () => {
try {
await api.deleteUser();
} catch (error) {
// ⚠️ Standard boundaries ignore this error!
// ✅ Use showBoundary to propagate it to the nearest boundary
showBoundary(error);
}
};
return (
<button onClick={handleAsyncAction}>
Delete User
</button>
);
}Performance & Production Tips #
1. The key Prop Trick for Resetting
#
When an error occurs, you often want to “retry.” However, if the underlying data causing the error hasn’t changed, React might just re-render the same broken tree.
A powerful pattern is to control the boundary with a key. When the key changes, React unmounts the old (broken) instance and remounts a fresh one.
function App() {
const [retryCount, setRetryCount] = useState(0);
return (
<ErrorBoundary
key={retryCount} // Changing this forces a hard remount
fallbackRender={({ resetErrorBoundary }) => (
<button onClick={() => {
// Logic to fix data...
setRetryCount(c => c + 1); // Triggers remount
resetErrorBoundary();
}}>Retry</button>
)}
>
<RiskyComponent />
</ErrorBoundary>
);
}2. Don’t Obscure Development Errors #
In development (NODE_ENV === 'development'), React will still show you the scary red error overlay even if you catch the error. This is intentional. Don’t try to hide it; click the “X” to see your fallback UI. In production builds, the overlay disappears, and your fallback UI appears instantly.
Conclusion #
In React 19, Error Boundaries remain a fundamental part of building resilient applications. While Server Components introduce new ways to handle data fetching errors on the server, the Client-Side Error Boundary is your last line of defense against a poor user experience.
Key Takeaways:
- Use
react-error-boundaryto avoid writing class components. - Wrap features, not just the app root.
- Remember that event handlers and async callbacks need
showBoundaryto trigger the UI fallback. - Always include Reset Logic so users aren’t stuck on an error screen forever.
Implement these patterns today, and your users—and your Sentry logs—will thank you.
Is your team migrating to React 19? Subscribe to React DevPro for next week’s deep dive into Server Actions and Optimistic UI updates.