React error boundaries: resilience and signals that scale
Isolating UI failures, shipping structured client logs, and keeping PII out of the payload—patterns that hold up in production.
Client-side exceptions are inevitable. Mature products contain the blast radius, give users a recovery path, and send engineering actionable, privacy-safe telemetry—not a minified stack trace with no context.
Where boundaries belong
I place boundaries at feature shells (a dashboard section, a checkout step), not around every leaf component. Too many boundaries fragment the tree; too few let a single child failure blank the entire route.
import { Component, type ErrorInfo, type ReactNode } from "react";
type Props = { children: ReactNode };
type State = { error: Error | null };
export class FeatureErrorBoundary extends Component<Props, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
const payload = {
name: error.name,
message: error.message,
componentStack: info.componentStack,
route: window.location.pathname,
release: process.env.NEXT_PUBLIC_APP_VERSION ?? "unknown",
};
void fetch("/api/client-log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).catch(() => undefined);
}
render() {
if (this.state.error) {
return (
<div role="alert" className="rounded-lg border p-4">
<p>This section could not be displayed.</p>
<button type="button" onClick={() => this.setState({ error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
Logging policy
Payloads exclude tokens, PII, and full free-text fields. I include route, release/build id, and a hash of the error class so incident tools can group events.
Takeaway: Treat boundaries, logging, and copy as one operability design—not three separate tickets.