To use a React error boundary with functional components, you still need one class: React has no hook that catches rendering errors, so the boundary itself has to be a class component even in an app made entirely of function components. You write that class once and wrap your function components in it. The usual answer in practice is the react-error-boundary package, which gives you the class plus a useErrorBoundary hook with showBoundary and resetBoundary for the errors boundaries cannot catch on their own. This guide builds both, with screenshots of a caught error, a recovery, and an event handler error being handed to the boundary.
Everything here was built and rendered with React 19.3.0 and react-error-boundary 6.1.6 in a Vite project, and each screenshot is the real component in a browser. Reference: catching rendering errors on react.dev.
Why a functional component cannot be an error boundary
Two class lifecycle methods make a boundary: static getDerivedStateFromError(), which switches to the fallback, and componentDidCatch(), which receives the error and the component stack for logging. There are no hook equivalents, which is the one thing the class-versus-hooks question still cannot resolve. So you write the class once and use it everywhere:
import { Component, useState } from "react";
// React has no hook for this: an error boundary has to be a class
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error }; // render the fallback on the next render
}
componentDidCatch(error, info) {
console.log("logged:", error.message, "| in", info.componentStack.trim().split("\n")[0]);
}
render() {
if (this.state.error) {
return (
<div style={{ fontFamily: "system-ui", padding: 12, background: "#fff4f4",
border: "1px solid #f3c2c2", borderRadius: 6, color: "#8a1f1f" }}>
<strong>Something went wrong in this section.</strong>
<div style={{ fontSize: 13, marginTop: 4 }}>{this.state.error.message}</div>
</div>
);
}
return this.props.children;
}
}
function SalesChart({ data }) {
return <p>Total: ${data.total.toFixed(2)}</p>; // data is undefined -> throws
}
export default function App() {
const [orders] = useState(null);
return (
<div style={{ fontFamily: "system-ui", padding: 16 }}>
<h3 style={{ margin: "0 0 8px" }}>Dashboard</h3>
<p>Orders today: 14</p>
<ErrorBoundary>
<SalesChart data={orders} />
</ErrorBoundary>
<p style={{ marginTop: 8 }}>The rest of the page keeps working.</p>
</div>
);
}
That containment is the point of boundaries. Without one, an error anywhere in the tree unmounts the whole app and leaves a blank page, which is what React 19 does by design when nothing catches it.
Giving people a way back: reset
A fallback that only apologises is a dead end. Put a button in it that clears the boundary’s state, and change whatever caused the failure at the same time, otherwise the child throws again immediately. The screenshot below was taken after a real click on Try again:
import { Component, useState } from "react";
class ErrorBoundary extends Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
reset = () => {
this.setState({ error: null });
this.props.onReset?.();
};
render() {
if (this.state.error) {
return (
<div style={{ fontFamily: "system-ui", padding: 12, background: "#fff4f4",
border: "1px solid #f3c2c2", borderRadius: 6 }}>
<p style={{ margin: "0 0 8px", color: "#8a1f1f" }}>{this.state.error.message}</p>
<button id="retry" onClick={this.reset}
style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #0b6bcb",
background: "#0b6bcb", color: "#fff", cursor: "pointer" }}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
function Report({ broken }) {
if (broken) throw new Error("Could not load the report");
return <p style={{ color: "#0a7d32" }}>Report loaded: 42 rows</p>;
}
export default function App() {
const [broken, setBroken] = useState(true);
return (
<div style={{ fontFamily: "system-ui", padding: 16 }}>
<ErrorBoundary key={String(broken)} onReset={() => setBroken(false)}>
<Report broken={broken} />
</ErrorBoundary>
</div>
);
}
Output:
[error] %o
%s
%s
Error: Could not load the report
at g (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:242:2590)
at Object.react_stack_bottom_frame (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:226:251)
at Ya (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:119:803)
at Ys (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:135:2458)
at gc (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:144:10556)
at w (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:33:108)
at Du (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:159:47479)
at wu (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:159:46235)
at Cu (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:159:46079)
at lu (http://127.0.0.1:52155/assets/index-BCG8GDWV.js:159:39287) The above error occurred in the <g> component. React will try to recreate this component tree from scratch using the error boundary you provided, h.
Note the key on the boundary. Changing it forces React to build a fresh boundary instance, which is the simplest way to clear the error state along with the data that caused it.
Using the react-error-boundary package
This is the library almost every codebase ends up with, because it saves rewriting the class and adds the pieces you would otherwise build yourself: FallbackComponent (or fallbackRender, or a plain fallback element), onError for logging, onReset to fix the cause, and resetKeys to reset automatically when a value changes:
npm install react-error-boundary
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert" style={{ fontFamily: "system-ui", padding: 12, background: "#fff8e6",
border: "1px solid #f0d08a", borderRadius: 6 }}>
<p style={{ margin: "0 0 8px" }}>Could not show the invoice: {error.message}</p>
<button id="retry" onClick={resetErrorBoundary}
style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #0b6bcb",
background: "#fff", color: "#0b6bcb", cursor: "pointer" }}>
Reload it
</button>
</div>
);
}
function Invoice({ id }) {
if (id === null) throw new Error("no invoice selected");
return <p>Invoice {id} is ready.</p>;
}
export default function App() {
const [id, setId] = useState(null);
return (
<div style={{ fontFamily: "system-ui", padding: 16 }}>
<ErrorBoundary
FallbackComponent={Fallback}
onError={(error) => console.log("sent to logging:", error.message)}
onReset={() => setId("INV-1001")} // fix the cause before retrying
resetKeys={[id]} // or reset automatically when id changes
>
<Invoice id={id} />
</ErrorBoundary>
</div>
);
}
Output:
[error] %o
%s
%s
Error: no invoice selected
at y (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:242:2411)
at Object.react_stack_bottom_frame (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:226:251)
at Ya (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:119:803)
at Ys (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:135:2459)
at gc (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:144:10557)
at w (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:33:108)
at Du (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:159:47490)
at wu (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:159:46243)
at Cu (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:159:46086)
at lu (http://127.0.0.1:62248/assets/index-BjS-xUvM.js:159:39290) The above error occurred in the <y> component. React will try to recreate this component tree from scratch using the error boundary you provided, p.
sent to logging: no invoice selected
onReset set a real invoice id, so the retry had something to show.Errors boundaries do not catch, and what to do
A boundary only catches errors thrown while React renders the tree below it. Four things fall outside that:
- Event handlers: a click handler that throws is ordinary JavaScript, not a render.
- Async code:
setTimeoutcallbacks, promise rejections andfetchhandlers run after the render finished. - Server-side rendering: boundaries are a client-side mechanism.
- The boundary itself: an error thrown inside the fallback goes to the boundary above it, if there is one.
useErrorBoundary() closes the first two. Catch the error yourself and call showBoundary(error), and the nearest boundary renders its fallback exactly as if the error had come from a render. The first button below handles the failure quietly; the second hands it over:
import { useState } from "react";
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
function SaveButton() {
const { showBoundary } = useErrorBoundary();
const [status, setStatus] = useState("ready");
const saveQuietly = () => {
try {
throw new Error("PUT /orders failed with 500");
} catch (error) {
console.log("caught in the handler, boundary never sees it:", error.message);
setStatus("failed silently");
}
};
const saveAndShow = () => {
try {
throw new Error("PUT /orders failed with 500");
} catch (error) {
showBoundary(error); // hand it to the nearest boundary
}
};
return (
<div style={{ fontFamily: "system-ui" }}>
<p id="status">status: {status}</p>
<button id="quiet" onClick={saveQuietly} style={{ marginRight: 8, padding: "6px 12px" }}>
Save (handled quietly)
</button>
<button id="show" onClick={saveAndShow} style={{ padding: "6px 12px" }}>
Save (show the boundary)
</button>
</div>
);
}
export default function App() {
return (
<div style={{ padding: 16 }}>
<ErrorBoundary fallback={<p style={{ fontFamily: "system-ui", color: "#8a1f1f" }}>
The save failed. Your changes are still here.</p>}>
<SaveButton />
</ErrorBoundary>
</div>
);
}
Console output from the first click:
caught in the handler, boundary never sees it: PUT /orders failed with 500
Which one you want depends on the failure. A validation message belongs next to the field; a save that lost the user’s work belongs in a boundary. For the render-time mistakes that get you here in the first place, see updating a component while rendering another and a component that does not render at all.
| Thrown where | Caught by a boundary? | What to do |
|---|---|---|
| Render of a child | Yes | Nothing, the fallback shows |
| A lifecycle method or effect | Yes | Nothing |
| An event handler | No | try/catch plus showBoundary() |
setTimeout or a promise | No | Catch it, then showBoundary() |
| Inside the fallback | No | A boundary further up |
| Server-side rendering | No | Handle it in the server framework |
More React guides worth reading:
- React hooks in class components
- Cannot update a component while rendering another
- React component not rendering
- React components with children
- Prevent a child component re-rendering
Frequently asked questions
Can I create an error boundary with a functional component?
No. getDerivedStateFromError and componentDidCatch exist only on classes. Write the class once, or use the react-error-boundary package.
What is useErrorBoundary used for?
It gives a function component showBoundary(error) to hand an error to the nearest boundary, and resetBoundary() to clear it.
Why does my error boundary not catch the error?
It probably came from an event handler, a timer or a promise. Catch it yourself and call showBoundary().
How do I reset a React error boundary?
Call resetErrorBoundary() from the fallback, pass resetKeys, or change the boundary’s key so React remounts it.
Where should I put error boundaries?
Around each independent section, so one failing widget does not blank the page, plus one at the top as a last resort.
Do error boundaries work in React 19?
Yes, unchanged. React 19 also adds onUncaughtError and onCaughtError options when you create the root, for logging.
Does react-error-boundary work with TypeScript?
Yes, it ships its own types, and the fallback receives a typed error and resetErrorBoundary.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn