Error handling

Where error boundaries live in the layout, what they catch, and how the chrome around your work stays usable when the content behind it crashes. The placement rules are locked in ADR-0030. This page is that ADR in plain words.

The placement test

One sentence drives every chrome decision we'll ever make:

Chrome that helps the user escape the app outlasts the boundary; chrome that decorates the app sits inside it.

In practice today: AppBar (sign out, switch tenant, head somewhere else), PreferencesCorner (turn off the motion or contrast that may have caused the crash), and CommsCorner (talk to a human about it) all outlast. Anything inside ContentWell sits inside.

Per-chrome boundaries — mandatory

The "outlasts" promise means nothing without per-chrome isolation. If CommsCorner sits outside the content boundary but has no boundary of its own, a Comms crash takes its siblings down with it — AppBar, PreferencesCorner, the route — and you're staring at a blank screen.

Every chrome pattern in @halo-compliance/ui self-isolates — the pattern wraps its own inner render in an <ErrorBoundary> with a pattern-local degraded fallback. Consumers don't have to remember to wrap. The contract is: every chrome stays in its silhouette under failure.

Four scopes

page
A route's render throws. Caught by SiteLayout's baked-in boundary. Fallback: full-ContentWell <ErrorState scope="page"> with Reload + the still-present CommsCorner outside the boundary.
section
A region inside content throws. Caught by a consumer-placed boundary around the region. Fallback: Card-sized inline <ErrorState scope="section">; the rest of the page is unaffected.
item
A single row / cell / list item throws. Caught by a consumer-placed boundary around the item. Fallback: tiny <ErrorState scope="item"> placeholder; the list keeps rendering.
chrome
A chrome pattern's render throws. Caught by the pattern's own boundary. Fallback is pattern-local — each chrome pattern owns its degraded shape because shapes diverge (an avatar circle is not a brand anchor is not a corner button).

Layouts declare their boundaries

Every canonical Layout component spells out the boundaries it bakes in. Layouts that ship without any boundary say so out loud — silence is worse than a stated "no boundary; consumer-owned," because the next person will assume one is included.

SiteLayout
Bakes in one page-level boundary around the ContentWell's children. AppBar, PreferencesCorner, and CommsCorner sit outside it and self-isolate independently.
PortalLayout (future)
Will bake in a page-level boundary around the centered entry task. Brand and silver corners stay outside.

The chrome-degraded principle

When a chrome pattern's boundary catches, the degraded fallback:

  • Preserves the silhouette. Same dimensions, same position. No layout shift.
  • Keeps it low-key. No panic banners. A chrome hiccup is a background event, not a headline.
  • Hints with a tooltip. Short, friendly, and points you toward recovery.
  • Preserves transport where possible. CommsCorner is load-bearing: its degraded state still opens the canonical mailto. The door must remain reachable — that's the entire point of the corner.

Consumer recipe

Page-level boundary comes free with SiteLayout. Section and item boundaries are consumer choices wherever a sub-tree might fail independently (a list of remote-data items; a widget that calls a flaky API).

A section that might failtsx
import { ErrorBoundary, ErrorState } from '@halo-compliance/ui'

<ErrorBoundary fallback={<ErrorState scope="section" title="Couldn't load risk profile" />}>
  <RiskProfileWidget vendorId={vendor.id} />
</ErrorBoundary>
A list where individual items may failtsx
import { ErrorBoundary, ErrorState } from '@halo-compliance/ui'

{vendors.map((v) => (
  <ErrorBoundary key={v.id} fallback={<ErrorState scope="item" title="Vendor unavailable" />}>
    <VendorRow vendor={v} />
  </ErrorBoundary>
))}
  • /error-boundary — the mechanism (Component); props, fallback contract, how each scope renders.
  • /error-states — the comprehensive Pattern home: when/where each scope applies, copy, severity.
  • /shell — SiteLayout's bakes-in callout for the page-level boundary.
  • ADR-0030 in design/adrs/ — the decision record this page renders.