Skip to content

ErrorBoundary

MD · 1950B gzip budget

Contains a render crash to its own subtree instead of the whole page, with a focusable, announced fallback.

Category
Feedback & status
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Contained content

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/error-boundary.json

Copies the source into your project. Pulls in 2 shared registry items: cn, announce.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { Component, useId, type ReactNode } from "react";
import { announce } from "../lib/announce";
import { cn } from "../lib/cn";

export interface ErrorBoundaryLabels {
  heading: string;
  retry: string;
}

const DEFAULT_LABELS: ErrorBoundaryLabels = {
  heading: "This part of the page couldn't load.",
  retry: "Try again",
};

interface ErrorBoundaryProps {
  children: ReactNode;
  labels?: Partial<ErrorBoundaryLabels>;
  /** Called after the user presses retry, before the boundary resets. */
  onReset?: () => void;
  /** Custom fallback. Receives the error and a reset function. */
  fallback?: (error: Error, reset: () => void) => ReactNode;
  className?: string;
}

interface State {
  error: Error | null;
}

/**
 * Contains a render crash to the subtree that caused it.
 *
 * The resilience thesis: one bad API response, one malformed date, one
 * component with a bug should cost the user the widget that broke — not the
 * page they were in the middle of using. React's own contract requires a
 * class component for this; there is no hook equivalent.
 */
export class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error): State {
    return { error };
  }

  componentDidCatch(error: Error) {
    announce(this.props.labels?.heading ?? DEFAULT_LABELS.heading, "assertive");
    // Bugs should be loud in development and silent-but-contained in
    // production — this is the one place in the library that should log.
    if (process.env.NODE_ENV !== "production") console.error(error);
  }

  reset = () => {
    this.props.onReset?.();
    this.setState({ error: null });
  };

  render() {
    const { error } = this.state;
    if (!error) return this.props.children;

    if (this.props.fallback) return this.props.fallback(error, this.reset);

    const labels = { ...DEFAULT_LABELS, ...this.props.labels };
    return <ErrorFallback labels={labels} onRetry={this.reset} className={this.props.className} />;
  }
}

function ErrorFallback({
  labels,
  onRetry,
  className,
}: {
  labels: ErrorBoundaryLabels;
  onRetry: () => void;
  className?: string;
}) {
  const headingId = useId();

  return (
    <div
      role="alert"
      aria-labelledby={headingId}
      className={cn(
        "flex flex-col items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-start dark:border-red-900/60 dark:bg-red-950/40",
        className,
      )}
    >
      <p id={headingId} className="text-sm text-red-900 dark:text-red-200">
        {labels.heading}
      </p>
      <button
        type="button"
        onClick={onRetry}
        className="rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-900 hover:bg-red-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:border-red-800 dark:text-red-100 dark:hover:bg-red-900/40"
      >
        {labels.retry}
      </button>
    </div>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation