Skip to content
Design Engineering
Product Behaviour

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

SituationInterface responseSystem responsibility
Invalid inputExplain the field and preserve the draft.Return safe, structured validation errors.
First read failsShow a persistent recovery panel.Bound retries and expose useful diagnostics privately.
Refresh failsKeep prior data when safe and show its age.Preserve cache scope and freshness rules.
Access deniedExplain the unavailable action without exposing private details.Enforce permission on the server.
Session expiresReauthenticate and restore safe context.Validate the return destination and draft policy.
Edit conflictsExplain what changed and let the person reconcile.Detect stale versions if the product requires it.
Write outcome unknownShow that confirmation is pending; avoid a blind repeat.Reconcile by operation ID or enforce idempotency.
Illustrative error result
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.

A missing response leaves the save outcome unknownA timeout leaves the outcome unknown. Check the operation’s result before offering a retry that could create a duplicate.DraftSaveSavingConfirmedTimed outSavedChecking the result…FoundKeep the draft until the outcome is known.
A timeout leaves the outcome unknown. Check the operation’s result before offering a retry that could create a duplicate.

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.

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

On this page