Error boundaries
Stop one broken component from taking down the whole page, and show a helpful fallback instead.
Bugs happen. Data arrives in a shape you did not expect, a value you assumed exists turns out to be missing, a library has an edge case. When that happens while React is rendering, the result is harsh: by default React removes the entire app from the screen. One broken book card means a blank page.
An error boundary is a component that catches errors thrown by components inside it and shows a fallback UI instead, while the rest of the app keeps working. Think of it as a try/catch for a part of your component tree.
When rendering fails
Click "Show covers" below. One book has bad data, and the cover component throws.
The preview shows an error instead of your app. The heading, the button and the unrelated paragraph are all gone. React does this on purpose: rather than leave a half-broken, possibly misleading UI on screen, it unmounts everything. Click the refresh icon on the preview to start over.
The fix for the bug is to handle missing colors. But you cannot foresee every bug, so you also want a safety net that limits the damage.
Catching errors with react-error-boundary
React's own docs are blunt: there is currently no way to write an error boundary as a function component. It has to be a class component, which this course otherwise never uses. You do not need to write that class yourself, though. The small, widely used react-error-boundary package provides a ready-made ErrorBoundary, and react.dev recommends it. Install it with npm install react-error-boundary.
Now only the part inside the boundary is replaced. The heading, the button and the paragraph survive. Notice that the whole boundary's content is replaced, including The Hobbit's cover, which was fine. A boundary swaps out everything inside it, so where you put it decides how much disappears.
In development, React still logs caught errors with console.error, and some sandboxes show that log. That does not mean the boundary failed. If the fallback is on screen, it worked. In production builds the log is the only trace, which is why you usually report errors to a logging service (see onError below).
Showing details and letting users recover
A fixed message is a start. Usually you want to show what went wrong and offer a way out. Instead of fallback, pass a FallbackComponent. It receives two props:
error: whatever was thrown. Its type isunknown, because JavaScript lets you throw anything, so you checkerror instanceof Errorbefore reading.message.resetErrorBoundary: a function that clears the error and renders the children again.
The boundary can also reset automatically when something changes. Pass resetKeys={[selectedId]}, and when selectedId changes while an error is showing, the boundary resets and tries again.
In the sandbox, the third book has no rating and crashes the details panel. Pick it, then pick another book.
Four props did the work:
FallbackComponentrenders the friendly panel with the real message.resetKeys={[selectedId]}resets the boundary when you pick a different book, so the user is never stuck on the error.onResetruns on every reset. Itsdetails.reasonis'keys'for aresetKeyschange and'imperative-api'whenresetErrorBoundarywas called, so the button can move back to a book that works.onErroris called once per caught error. In a real app you send the error to a service like Sentry here.
What boundaries do not catch
Error boundaries catch errors thrown while React renders components inside them. They do not catch errors thrown:
- inside event handlers, like
onClick; - in asynchronous code, like a
setTimeoutcallback or after anawait; - in the error boundary component itself.
That is because those errors do not happen during rendering. React is not on the call stack, so it cannot catch them. For event handlers, a normal try/catch is often enough. When you want such an error to show the nearest boundary's fallback, react-error-boundary gives you the useErrorBoundary hook. Its showBoundary(error) function hands the error to the nearest boundary.
This one fails about half the time, so click "Load reviews" a few times. Here "Try again" makes sense, because the failure is random, like a flaky network. fallbackRender is a third way to give a fallback: a function that receives the same props as FallbackComponent.
useErrorBoundary must be called in a component inside an ErrorBoundary, or it throws. Data libraries can also do this for you: TanStack Query's useQuery accepts throwOnError: true, which sends failed queries to the nearest boundary.
Under the hood: the class version
For the curious, this is roughly what an error boundary looks like if you write it yourself. It is the only class component in this course, and you do not need to memorize it.
import { Component } from 'react';
import type { ErrorInfo, ReactNode } from 'react';
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
// Runs during rendering when a child throws: switch to the fallback.
static getDerivedStateFromError(): State {
return { hasError: true };
}
// Runs after the error is caught: a good place to log it.
componentDidCatch(error: unknown, info: ErrorInfo) {
console.error(error, info.componentStack);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}The two special methods, static getDerivedStateFromError and componentDidCatch, have no hook equivalents yet. That is the whole reason error boundaries are still classes. react-error-boundary is this same idea plus resetting, fallback components and the showBoundary hook.
Deep diveWhere to put boundaries
There is no single right answer, but a useful rule of thumb is: one boundary per thing that can fail independently.
- One top-level boundary around the whole app, so the user always sees "Something went wrong, reload" instead of a blank page.
- One per page or route, so a broken detail page leaves the navigation working. You will add exactly this to the Reading List in the checkpoint, with
resetKeys={[id]}so moving to another book clears the error. - One per independent widget, like a sidebar or a reviews panel, when the rest of the page is still useful without it.
Wrapping every small component is too much: users end up with a page full of tiny error messages. React 19 also lets you pass onCaughtError and onUncaughtError options to createRoot, which is a central place to report errors from every boundary at once.
Keep the dashboard alive
This dashboard has three widgets. The Streak widget crashes. Right now that blanks the entire dashboard. Add error boundaries so that only the broken widget shows a small "This widget failed to load" message, and the other two keep working.
Show solution
import type { ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { CurrentlyReading, Goal, Streak } from './widgets';
function Widget({ children }: { children: ReactNode }) {
return (
<ErrorBoundary fallback={<section className="card failed">This widget failed to load.</section>}>
{children}
</ErrorBoundary>
);
}
export default function App() {
return (
<div className="grid">
<Widget>
<CurrentlyReading />
</Widget>
<Widget>
<Streak />
</Widget>
<Widget>
<Goal />
</Widget>
</div>
);
}Each widget gets its own boundary, so a failure is contained to one card. Wrapping the boundary in a small Widget component keeps App readable, and gives you a single place to add logging later with onError. One boundary around the whole grid would have replaced all three widgets with a single message.
A button's onClick handler throws an error. The button is inside an ErrorBoundary. What happens?
Why is an error boundary still a class component in React 19?
Day 5 progress