<!-- Protocore Design System — ErrorState -->
# ErrorState

- **Category:** Feedback (`feedback`)
- **Slug:** `feedback/error-state`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { ErrorState } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/feedback/error-state

> A centered failure panel with a danger accent rule, optional debugId tag and a retry affordance.

## The five-state doctrine

**ErrorState** is state 3 of the five — *loading, empty, error, pending, data*. It means the request **failed**. Two rules keep it honest:

1. **Always offer a way forward.** Wire `onRetry` for anything transient; a dead end with no retry is the most common error-handling drift.
2. **Always carry a `debugId`.** The request id, trace id, or correlation id turns "it's broken" into a searchable support ticket.

Don't confuse this with **empty** — a fetch that succeeds with zero rows is an [EmptyState](/feedback/empty-state), and dressing a failure as "No data" hides the problem from the user *and* your logs.

## ErrorState vs. Banner vs. Toast

Pick by scope and blast radius:

- **ErrorState** — the whole panel/view has no content to show because loading it failed. It *replaces* the content.
- **[Banner](/feedback/banner)** — the surface still works, but there's a persistent, page-level problem to flag (degraded region, expired token).
- **[Toast](/feedback/toast)** — a transient failure of a discrete action ("Couldn't save") that doesn't block the rest of the view.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `debugId` | `string` | no | — | Machine debug identifier, rendered as a mono metadata tag. |
| `detail` | `ReactNode` | no | — | Secondary explanatory copy. |
| `eyebrow` | `ReactNode` | no | `ERROR` | Mono uppercase eyebrow above the title. |
| `icon` | `ReactNode` | no | — | Optional decorative icon slot, rendered above the eyebrow. |
| `onRetry` | `(() => void)` | no | — | When provided, renders a retry button that calls this handler. |
| `retryLabel` | `string` | no | `Retry` | Label for the retry button. |
| `style` | `CSSProperties` | no | — | — |
| `title` | `ReactNode` | no | `Something went wrong` | Sentence-case headline. |

## Examples

### Basics

A `title` and `detail` under an `ERROR` eyebrow. Both default sensibly, so the smallest useful ErrorState is nearly propless.

```tsx
import { ErrorState } from "@protocore/pds";

export default function Basics() {
  return (
    <ErrorState
      title="Couldn’t load the ledger"
      detail="The indexer returned no response. This is usually transient."
    />
  );
}
```

### Retry and debugId

Pass `onRetry` to render a retry button, and a `debugId` to surface the request/trace id as a mono metadata tag — the string support will ask for.

```tsx
import { useState } from "react";
import { ErrorState, InlineMessage } from "@protocore/pds";

export default function WithRetry() {
  const [attempts, setAttempts] = useState(0);

  if (attempts > 0) {
    return <InlineMessage tone="info">Retrying… (attempt {attempts})</InlineMessage>;
  }

  return (
    <ErrorState
      title="Sync failed"
      detail="Block height diverged from the canonical chain."
      debugId="req_8f21c4a0"
      onRetry={() => setAttempts((n) => n + 1)}
      retryLabel="Retry sync"
    />
  );
}
```

## Do & don't

**Do**

- Wire onRetry for anything that might succeed on a second attempt.
- Always surface a debugId so failures are traceable.
- Write a plain-language title; put the technical cause in detail.
- Use it when loading the surface failed and there's nothing to show.

**Don't**

- Leave a failed surface with no retry and no id — a silent dead end.
- Dump a raw stack trace or HTTP status as the title.
- Use ErrorState for a transient action failure — that's a Toast.
- Show "No data" (EmptyState) when the request actually errored.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Moves focus to the retry button when one is rendered. |
| `Enter / Space` | Activates the focused retry button. |

**Notes**

- The root is `role="alert"`, so its content is announced assertively when it appears.
- The `icon` slot is decorative (`aria-hidden`); the message lives in the title and detail.
- The `debugId` renders as visible mono text, so it can be read aloud and copied — not hidden in an attribute.
- The retry control is a real `<button>` with full keyboard and focus-ring support.

## Related

`empty-state`, `loading-skeleton`, `banner`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
