Chapter 3 of 4
Placing Boundaries
Granularity, recovery, and what the user actually sees.
Where you put boundaries decides how much of the screen a single failure takes with it.
A layered approach
- One boundary at the root, so a total failure still shows a branded page rather than a blank screen.
- One per route, so navigating away recovers naturally.
- One around each independent widget - a chart, a feed, a third-party embed - so one failure does not remove the rest of the page.
<RootErrorBoundary>
<Layout>
<RouteErrorBoundary>
<Dashboard>
<ErrorBoundary fallback={<WidgetError name="Revenue chart" />}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Activity" />}>
<ActivityFeed />
</ErrorBoundary>
</Dashboard>
</RouteErrorBoundary>
</Layout>
</RootErrorBoundary>Recovering
A boundary that only ever shows a dead end is frustrating. Give the user a way out, and reset the boundary's state so children can try again.
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) { return { error }; }
reset = () => this.setState({ error: null });
render() {
if (this.state.error) {
return (
<div role="alert">
<p>This section could not be loaded.</p>
<button onClick={this.reset}>Try again</button>
</div>
);
}
return this.props.children;
}
}Logging
componentDidCatch is where errors reach your monitoring tool. Include the component stack - it tells you which component failed, which the JavaScript stack often does not.
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, {
contexts: { react: { componentStack: errorInfo.componentStack } },
});
}Write a safe error message formatter
Complete errorMessage so it returns error.message when it exists, and a generic fallback otherwise. It must never throw.