Design error states and recovery workflows that guide users to resolution. Learn context-aware error messages, graceful degradation, and recovery patterns...
Turn every failure into a guided path forward: find each way the app can fail, give it a specific human message, a correct placement, and at least one recovery action. The prime directive: never blame the user, and never leave them at a dead end β every error state ships with a way out. Deliver an error-state matrix covering the failure modes plus the implemented components and copy β not messaging guidelines alone.
Use for: error message copy, validation UX, network/offline failure handling, permission-denied and 404 pages, recovery workflows, graceful degradation, error accessibility.
Hand off instead when the real need is:
skills/frontend-design/performance-optimizationskills/frontend-design/loading-states β this skill owns what happens when loading failsskills/frontend-design/interaction-physicsskills/frontend-design/accessibility-excellencecatch, .catch(, onError, error state variables, toast.error, alert(, error boundary components, and string literals like "error", "failed", "wrong", "invalid". Collect the actual message strings β the current copy is your before-picture.role="alert", aria-live, aria-invalid, aria-describedby. Absence = automatic fix items.Ask the user (one batch, only if not inferable): the support channel to point to (email/help center β needed for system-error copy), and whether any operations are high-stakes (payments, destructive actions) needing extra-careful recovery. If unanswerable, use a placeholder support link, flag it, and proceed.
For every failure mode found in Step 0, fill one row: failure mode β what the user was doing β message (what happened / why / what to do) β placement β recovery action(s) β a11y wiring. This matrix is the audit deliverable and the implementation checklist.
Copy rules: never blame ("you entered an invalidβ¦" β "please enter aβ¦"); state the fix, not just the rule; keep the user's data intact in the message's promises only if the app actually preserves it β never claim "we saved your work" unless it's true.
| Class | Placement | Timing | Recovery |
|---|---|---|---|
| Validation | Inline, adjacent to the field | On blur or submit β never on each keystroke of an incomplete entry; clear immediately once fixed | Fix guidance; shortcut when it exists ("Sign in instead") |
| Network | Section-level state or toast; full state if the whole view failed | On failure | Retry button; preserve entered data; offline continuation if supported |
| Permission | Full state replacing the blocked content | On load/action | Explain why + "Request access" or path to the owner |
| System/5xx | Section or page state | On failure | Retry + support contact + error ID |
| 404 | Dedicated page | On route | Search + links to key destinations |
Fork rules:
role="alert" (task-blocking) or aria-live="polite" (background).aria-invalid="true" + aria-describedby pointing at the message element.references/patterns.md β read it when writing the code.Force each failure (invalid input, network offline in devtools, a 500 via bad endpoint, an unknown route) and confirm: message renders in the right place, recovery action works, entered data survives, screen reader announcement fires (or the wiring is present), nothing fails silently.
Deliver both artifacts:
1. The code β implemented error components, copy strings, error boundaries, and a11y wiring, edited into the app's existing component and styling conventions.
2. Error-State Matrix (markdown):
## Error-state matrix
| # | Failure mode | Class | Message (final copy) | Placement | Recovery | A11y | Status |
| 1 | Signup email taken | validation | "Email already in useβ¦" | inline under field | "Sign in instead" link | aria-invalid + describedby + role=alert | implemented |
| 2 | Feed fetch fails | network | "We couldn't load your feedβ¦" | section state | Retry | polite live region | implemented |
| 3 | Card declined | system | "Your card was declinedβ¦" | modal | Try again / other method | alertdialog, focus trapped | implemented |
## Silent failures fixed
| Location | Was | Now |
## Copy changes
| Before | After |
## Assumptions
[Support contact used, failure modes you could not reproduce, etc.]
role="alert"/aria-live, aria-invalid, aria-describedby, and focus management implementedcatch block in touched code swallows an error without a user-facing or logged consequenceHard don'ts: don't promise recovery the app can't deliver ("your work is saved" without persistence); don't use humor for destructive or payment failures; don't add error UI that hides the actual error from developers β keep console/telemetry logging intact.
skills/frontend-design/loading-states β hands you its error-empty placements; you expand them into full messages + recovery. The loadingβerror transition should reuse the same layout slot.skills/frontend-design/performance-optimization β optimistic-UI rollbacks from that skill need your failure toasts/messages when a background sync fails.skills/frontend-design/component-architecture β provides the form/input components you add error variants to; add ErrorState as a shared component there.skills/frontend-design/accessibility-excellence β audits your live-region and focus behavior; build to its standards up front.references/patterns.md β badβgood message rewrites, complete markup for each failure class (validation, network, permission, system, 404), inline/modal/progressive recovery patterns, graceful-degradation snippets, error styling CSS, and the a11y wiring blocks. Read it when writing the components and copy.