Error Boundary

A React error boundary that catches render-phase errors thrown by its subtree and replaces the broken region with a fallback. Wrap risky leaves — anything that hits the network mid-render, parses user input, or runs untrusted code — so a single bad render can't tear down the whole tree.

All systems nominal.

Customize
Fallback

Installation

npx shadcn@latest add https://craftbits.dev/r/error-boundary.json

Usage

import { ErrorBoundary } from "@craft-bits/core";
 
<ErrorBoundary>
  <RiskyTree />
</ErrorBoundary>

Pass a custom fallback as a node, a render-prop, or a component:

<ErrorBoundary fallback={<p>Something broke.</p>}>
  <RiskyTree />
</ErrorBoundary>
 
<ErrorBoundary
  fallback={({ error, reset }) => (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
>
  <RiskyTree />
</ErrorBoundary>

Wire telemetry through onError, and auto-recover when route or input changes via resetKeys:

<ErrorBoundary
  onError={(err, info) => telemetry.report(err, info)}
  onReset={() => analytics.track("error_recovered")}
  resetKeys={[route, userId]}
>
  <RiskyTree />
</ErrorBoundary>

Understanding the component

  1. Class under the hood, forwardRef on top. React's error-handling lifecycles only exist on class components. ErrorBoundary is exported as a forwardRef-ed functional wrapper around a private class — the forwarded ref lands on the root <div> of the rendered fallback so callers can scroll-to or measure the error region.
  2. Fallback polymorphism. The fallback prop accepts a ReactNode for static content, an ErrorBoundaryFallbackRender render-prop that receives { error, reset }, or a ComponentType<ErrorBoundaryFallbackProps> when the caller already has a dedicated fallback component.
  3. Auto-reset via resetKeys. When any value in resetKeys changes between renders the boundary clears its error state and re-renders children. Use this to recover automatically when the route, the requested resource, or the form input changes.
  4. onError is a side channel. It fires once per caught error and exists for telemetry. It cannot recover from the error — the boundary always renders the fallback.
  5. Default screen. With no fallback prop, the boundary renders DefaultErrorScreen: a polished card with an alert glyph, a headline, body copy, an optional diagnostic block, and a primary retry button. All copy is overridable.
  6. Data hooks. The root carries data-cb-error-boundary and data-state="error". The diagnostic block carries data-cb-error-boundary-details.

Variants

Default fallback

<ErrorBoundary>
  <RiskyTree />
</ErrorBoundary>

Custom render-prop fallback

<ErrorBoundary
  fallback={({ error, reset }) => (
    <div role="alert">
      <h2>Couldn't load this section.</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )}
>
  <RiskyTree />
</ErrorBoundary>

Auto-reset on route change

<ErrorBoundary resetKeys={[pathname]}>
  <RoutedPanel />
</ErrorBoundary>

Telemetry hook

<ErrorBoundary onError={(err, info) => sentry.captureException(err, { extra: info })}>
  <RiskyTree />
</ErrorBoundary>

Props

ErrorBoundary

PropTypeDefaultDescription
childrenReactNodeThe subtree protected by the boundary.
fallbackReactNode | ErrorBoundaryFallbackRender | ComponentType<ErrorBoundaryFallbackProps>Custom fallback. Omit to use DefaultErrorScreen.
onError(error: Error, info: ErrorInfo) => voidSide-channel callback for telemetry.
onReset() => voidFires when the boundary transitions back from error to ok.
resetKeysreadonly unknown[]Auto-reset when any value here changes.
showErrorDetailsbooleanNODE_ENV !== "production"Show error message in a diagnostic block on the default fallback.
classNamestringMerged onto the rendered fallback root via cn().
...restHTMLAttributes<HTMLDivElement>Any other <div> attribute.

ErrorBoundaryFallbackProps

FieldTypeDescription
errorErrorThe error that was caught.
reset() => voidClears the boundary and re-renders children.

DefaultErrorScreen

PropTypeDefaultDescription
errorErrorThe caught error.
reset() => voidRetry callback.
showErrorDetailsbooleantrueRender the error message in a diagnostic block.
titleReactNode"Something went wrong"Headline copy.
descriptionReactNoderetry hintBody copy.
retryLabelstring"Try again"Retry button label.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The rendered fallback root is a role="alert" region with aria-live="assertive" so assistive tech announces the error immediately when the boundary trips.
  • The retry button on the default screen has a 44 px minimum hit area and a visible :focus-visible ring keyed to --cb-accent.
  • The alert glyph is aria-hidden so screen readers don't double-announce it alongside the headline.
  • Colour contrast: every text token (--cb-fg, --cb-fg-muted) clears WCAG AA on --cb-bg-elevated in both light and dark themes.
  • No motion. Reduced-motion respect is the responsibility of the surrounding content.

Credits

  • Extracted from: algoflashcards (src/platform/ui/ErrorBoundary.tsx). The original used hardcoded hex fallbacks for error tone, threaded the dev-only diagnostic toggle through import.meta.env.DEV, and shipped an internal ErrorScreen with a hard-coded "Go home" link. craft-bits replaces the colours with cb-error / cb-accent tokens, swaps the dev gate for a NODE_ENV-driven showErrorDetails prop the caller can override, drops the project-specific "Go home" link, and adds onError, onReset, and resetKeys so the boundary is composable with telemetry and route-driven recovery.