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.
<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.
<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
{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 | itemREQUIRED Which content scope. The chrome scope isn't a value here — chrome fallbacks live in each chrome pattern.
- title
stringOverride the default title. Defaults vary by scope (e.g. page → "This page hit an error").
- message
ReactNodeOverride the default body. Defaults explain "the rest still works."
- error
ErrorThe captured Error. Rendered inside a
<details>for technical detail. Wire from the ErrorBoundary render-prop.- onReset
() => voidRecovery affordance. Page scope defaults to
window.location.reload()when absent.- resetLabel
stringOverride 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
onErrorcallback that ships to Sentry. The user-facing fallback is one thing; the developer report is another.
Related
- /error-handling — placement doctrine (Layout).
- /error-boundary — mechanism (Component).
- /shell — SiteLayout's baked-in page boundary.
- /card — section-scope leverages Card variant="critical".