Error states

Your go-to fallback family. Three content scopes — page, section, item — pair with <ErrorBoundary> to replace failed sub-trees. The fourth scope, chrome, isn't rendered here — each chrome pattern owns its own degraded shape.

BetaErrorState is Beta — the pattern's component, shipped in @halo-compliance/ui and paired with ErrorBoundary for the catch. The three content scopes are settled; the prop surface may still move.

This page is the one-stop shop: when each scope kicks in, what your user sees, and what each one lets them do. The placement principle lives in /error-handling; the React mechanism lives in /error-boundary; both live side by side here.

Every demo below is live: tripping a fallback makes a child component really throw during render, a real ErrorBoundary really catches it, and the recovery affordance really resets the boundary. (React reports each catch to the browser console — that's the mechanism working, not a defect.)

scope="page"

Triggered when a route's render throws. SiteLayout's baked-in boundary catches it. Takes the entire ContentWell so the user knows the page they wanted isn't reachable; AppBar + Preferences + Comms stay present (outside the boundary) so they can navigate elsewhere or report it.

The route rendered — this is the healthy subtree. Trip the fallback to throw during render and watch the page-scope shape take the frame; Reload page recovers it in place here (in production the default affordance reloads the document).

tsx
<ErrorBoundary fallback={<ErrorState scope="page" />}>
  {routeChildren}
</ErrorBoundary>

scope="section"

Triggered when a region inside content throws. Caught by a consumer-placed boundary around the region. A card-sized inline error in the critical Card variant — its solid critical fill makes the failure pop at a glance without taking over the page.

Vendor risk profile loaded — a healthy region. Trip the fallback to throw inside the region's boundary; Try again resets it.

tsx
<ErrorBoundary
  fallback={
    <ErrorState
      scope="section"
      title="Couldn't load vendor risk profile"
      onReset={refetch}
    />
  }
>
  <RiskProfileWidget vendorId={vendor.id} />
</ErrorBoundary>

scope="item"

Triggered when a single row / cell / list item throws. Caught by a per-item boundary so the rest of the list keeps rendering. Tiny inline placeholder with click-to-retry semantics.

A vendor list — trip the fallback to fail one row while the rest keep rendering:

Vendor — Stripe

Vendor — Twilio

Vendor — Fastly

Vendor — Datadog

tsx
{vendors.map((v) => (
  <ErrorBoundary
    key={v.id}
    fallback={<ErrorState scope="item" title="Vendor row unavailable" />}
  >
    <VendorRow vendor={v} />
  </ErrorBoundary>
))}

scope="chrome" — pattern-local, not rendered here

Each chrome pattern owns its own broken-state look because the shapes just don't match — an avatar circle, a brand anchor, and a corner button have nothing visual in common. The ground rules hold everywhere: keep the layout in place, keep the fallback quiet, add a tooltip hint, and (for Comms specifically) keep the transport working.

BrandSlot degraded
Plain text "Halo" in place of the artwork; anchor to home still works.
AccountSlot degraded
Empty avatar circle, no menu, tooltip explains.
PreferencesCorner degraded
Silver button in the corner, inert, tooltip explains.
CommsCorner degraded
Stays functional via direct mailto — the popover is bypassed, but the human-comms door still opens. The transport is the load-bearing concern of the corner.

Props

scope
page | section | item

REQUIRED Which content scope. The chrome scope isn't a value here — chrome fallbacks live in each chrome pattern.

title
string

Override the default title. Defaults vary by scope (e.g. page → "This page hit an error").

message
ReactNode

Override the default body. Defaults explain "the rest still works."

error
Error

The captured Error. Rendered inside a <details> for technical detail. Wire from the ErrorBoundary render-prop.

onReset
() => void

Recovery affordance. Page scope defaults to window.location.reload() when absent.

resetLabel
string

Override the recovery action label. Defaults: page → "Reload page", section → "Try again", item → "Retry".

When to reach for which

  • page — almost never explicitly. SiteLayout supplies one already. Hand-build a page-scope ErrorBoundary only if you're rolling your own Layout.
  • section — around any region whose failure is independent of the surrounding page: a remote-data widget, a chart, a paneled sub-view.
  • item — around list items where each can fail individually. The rest of the list keeps rendering.

Anti-patterns

  • Don't show item-scope copy at page scope. "Error" alone is not enough for a page-takeover; the user needs to know what to do.
  • Don't put fetches in the fallback. A fallback that throws crashes its own boundary's parent. Keep fallbacks declarative.
  • Don't hide the error from telemetry. Pair every ErrorBoundary with an onError callback that ships to Sentry. The user-facing fallback is one thing; the developer report is another.