ComponentsFeedback

Error boundary

What shows when a page can't: a crash, a missing page, no access, offline, signed out or the database out of reach. The failure stays inside the smallest part that broke, keeps a way out, and is reported on its own.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

import { ErrorFallback } from '@/ui/error-boundary';<ErrorFallback />

A page failed

The sidebar and the page header stay; only the content area is replaced, so you can go somewhere else. The same parts as the empty state: the 32px tile, a 16/20/600 title, one line and one way out. Try again is secondary: it is a retry, not the page's main action.

Companies
import { ErrorFallback } from '@/ui/error-boundary';

// The Companies page failed: the header stays, only the content area is replaced.
export default function ErrorBoundarySection() {
  return (
    <div className="flex w-full flex-col overflow-hidden rounded-xlarge shadow-raised">
      <div className="flex h-14 items-center border-b border-border px-300 text-heading-small leading-body font-semibold tracking-heading-small text-text">Companies</div>
      <div className="flex min-h-80 items-center justify-center">
        <ErrorFallback onRetry={() => {}} focusAction={false} />
      </div>
    </div>
  );
}

Every page state

Each state says what happened, what to do and what was kept, and has a way out: Try again for a crash, the database out of reach or offline; Go home for a page that isn't found; Switch agency and Sign out for someone who isn't a member of this agency; Sign in again when the session ended, with the draft kept. A crash shows its error ID small, so support can find the report. A page the member's role can't see shows Page not found, so its existence isn't revealed.

import { ErrorFallback } from '@/ui/error-boundary';

// Every page state with its way out. The crash shows its error ID for support.
export default function ErrorBoundaryProblems() {
  const card = 'rounded-xlarge shadow-raised';
  return (
    <div className="grid w-full grid-cols-1 gap-300 md:grid-cols-2">
      <ErrorFallback problem="crash" onRetry={() => {}} eventId="4f9c1e2a7b6d4c1f" focusAction={false} className={card} />
      <ErrorFallback problem="not-found" homeHref="#today" focusAction={false} className={card} />
      <ErrorFallback problem="no-access" agency="Northbridge" onSwitchAgency={() => {}} onSignOut={() => {}} focusAction={false} className={card} />
      <ErrorFallback problem="unavailable" onRetry={() => {}} focusAction={false} className={card} />
      <ErrorFallback problem="signed-out" signInHref="#sign-in" focusAction={false} className={card} />
      <ErrorFallback problem="offline" onRetry={() => {}} focusAction={false} className={card} />
    </div>
  );
}

The app failed

Only when the root layout itself throws. There is no sidebar to keep, so one sentence replaces the whole document.

import { ErrorFallback } from '@/ui/error-boundary';

// Only when the root layout itself throws: one sentence in place of the whole document.
export default function ErrorBoundaryApp() {
  return (
    <div className="flex min-h-72 w-full items-center justify-center rounded-xlarge shadow-raised">
      <ErrorFallback scope="app" />
    </div>
  );
}

A panel failed

PanelError takes the place of one panel of a record page, as an error card with Try again and the error ID, while the rest of the page keeps working.

import { PanelError } from '@/ui/error-boundary';

// One panel of a record page failed; the card takes its place and the rest of the page keeps working.
export default function ErrorBoundaryPanel() {
  return <PanelError className="max-w-120" onRetry={() => {}} eventId="9b1d7c3e5a2f4e8d" />;
}

Catch a crash

ErrorBoundary around each panel of a record page. Press Break the notes: only the Notes panel shows the error card, the details beside it keep working, and Try again draws the notes again.

Maya Chen

Senior Data Engineer at Halcyon Bank

Notes

Called Maya on Tuesday: open to a move in January.

import { useState } from 'react';
import { Button } from '@/ui/button';
import { ErrorBoundary } from '@/ui/error-boundary';

function Notes({ broken }: { broken: boolean }) {
  if (broken) throw new Error('Notes failed to draw');
  return <p className="text-body leading-body text-text">Called Maya on Tuesday: open to a move in January.</p>;
}

// A record page with two panels. Press Break the notes: only that panel shows the error card; the details keep working.
export default function ErrorBoundaryLive() {
  const [broken, setBroken] = useState(false);
  return (
    <div className="flex w-full flex-col items-start gap-200">
      <div className="grid w-full grid-cols-1 gap-200 md:grid-cols-2">
        <section aria-label="Details" className="flex flex-col gap-100 rounded-xlarge p-200 shadow-raised">
          <h3 className="text-heading-small leading-body font-semibold text-text">Maya Chen</h3>
          <p className="text-body leading-body text-text-subtle">Senior Data Engineer at Halcyon Bank</p>
        </section>
        <section aria-label="Notes" className="flex flex-col gap-100 rounded-xlarge p-200 shadow-raised">
          <h3 className="text-heading-small leading-body font-semibold text-text">Notes</h3>
          <ErrorBoundary onReset={() => setBroken(false)} onError={() => '9b1d7c3e5a2f4e8d'}>
            <Notes broken={broken} />
          </ErrorBoundary>
        </section>
      </div>
      <Button size="sm" onClick={() => setBroken(true)}>
        Break the notes
      </Button>
    </div>
  );
}

On this page