Open chat

← All posts

Reactobservability

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.

← Back to portfolio