Design failure and recovery
Distinguish what failed, what remains safe to use, and what the person can do next.
Why this matters
A good error state answers three questions: what happened, what happened to my work, and what can I do now?
Start from the action and what it risked. A foreman reassigns tomorrow's shift and the refresh fails, an edit gets rejected, a save times out — each needs different recovery even if all three show a network error.
What to understand
An error contract is the agreed exchange for failure: what you get back when it breaks. Machine-readable codes let the UI pick recovery without parsing sentences. Keep diagnostic detail on the server and return only safe information.
“No results” isn't always an error. Tell an empty crew list apart from filters that matched nothing, so the next step makes sense.
Watch for
- Repeating an unchanged bad request or a denied permission as if retry helps.
- A blind repeat of a save whose outcome is unknown.
- A disabled button standing in for server-backed safe retry.
- An important failure living only in a vanishing toast.
- Stack traces or private diagnostics leaking into the UI.
- Claiming work is safe without saved data that survives reload.
Strong default
Retry safe reads a few times with growing pauses; respect rate limits and stop rules. For saves, name one logical save with a server-backed safe-retry key so doing it twice has the same effect as once. Keep prior data when safe, input preserved, and the rest of the screen usable.
When this doesn't apply
Not every failure wants a retry. An unchanged bad request or denied permission won't improve by repeating it. Error boundaries catch rendering failures; handle request and click errors where the action happens instead.
In practice
| Situation | Interface response | System responsibility |
|---|---|---|
| Invalid input | Explain the field and preserve the draft. | Return safe, structured validation errors. |
| First read fails | Show a persistent recovery panel. | Bound retries and expose useful diagnostics privately. |
| Refresh fails | Keep prior data when safe and show its age. | Preserve cache scope and freshness rules. |
| Access denied | Explain the unavailable action without exposing private details. | Enforce permission on the server. |
| Session expires | Reauthenticate and restore safe context. | Validate the return destination and draft policy. |
| Edit conflicts | Explain what changed and let the person reconcile. | Detect stale versions if the product requires it. |
| Write outcome unknown | Show that confirmation is pending; avoid a blind repeat. | Reconcile by operation ID or enforce idempotency. |
type SaveFailure =
| { code: 'VALIDATION'; fields: Record<string, string> }
| { code: 'FORBIDDEN' }
| { code: 'CONFLICT'; currentVersion: number }
| { code: 'UNAVAILABLE'; requestId: string };The codes and fields must match your actual API. A request ID can help support find hidden diagnostics; it should never carry a secret or personal data. Ask your agent: "do these codes match what the server really sends?"
A safe read can usually retry a few times with growing pauses. Respect rate limits and stop rules. An unchanged bad request or a denied permission won't improve by repeating it.
For saves, “no response” doesn't mean “didn't save.” A safe-retry key names one logical save so the server can return the same result without doing the work twice. It needs server support and a set lifetime; a disabled button alone can't do this.
Use field errors for input, a lasting section message for missing data, and a page fallback when the route can't work. Toasts can confirm small background events, but never as the only place an important failure lives.
Use calm, specific copy: “Your changes are still here. We could not confirm the save.” Don't blame the person, show stack traces, or claim work is safe unless saved data that survives reload actually guarantees it.
Error boundaries catch rendering failures; handle request and click errors where the action happens. Keep the rest of the screen usable where you can.
Verify
Recovery action, kept input, focus behavior, and final saved result — not just the message. Failed first read, failed refresh, conflict, expired session, and unknown-outcome save each replayed against the contract.
Related skills
Use $states to inspect existing work. Check the recovery action, kept input, focus behavior, and final saved result — not just the message.
Last updated on