An error is a state
A failed request is not an exception to the interface. It is one of the states the interface must represent alongside loading, empty, stale, and partially available data.
The message should answer three things: what failed, whether the user's work was preserved, and what action is safe now. "Something went wrong" answers none of them.
Model the states explicitly
A form submission may be idle, validating, submitting, succeeded, or failed. A data panel may be loading, stale, empty, unavailable, or ready. These states should be represented in the view model instead of inferred from a collection of booleans that can contradict one another.
stateDiagram-v2
[*] --> Idle
Idle --> Validating: submit
Validating --> Invalid: input rejected
Validating --> Submitting: input valid
Submitting --> Succeeded: server confirms
Submitting --> Failed: recoverable error
Failed --> Submitting: retry
Invalid --> Validating: edit and submit
Preserve user work
When a request fails, keep the entered data and the server's correlation ID. If the action is safe to retry, show the retry control close to the failed operation. If the error needs a correction, place the user at the field or rule that needs attention rather than resetting the whole form.
type SubmitState =
| { status: "idle" }
| { status: "submitting" }
| { status: "error"; message: string; requestId: string }
| { status: "success"; resourceId: string };
Avoid false certainty
Do not show a green success state before a durable write is confirmed unless the product explicitly calls it optimistic. For actions with external side effects, a pending state is safer than a success toast that later turns out to be untrue.
Preserve agency
A retry button is useful when the operation is safe to repeat. An edit link is useful when input is invalid. A support reference is useful when the system needs human help. The recovery action should match the failure mode.
Good error handling reduces support work because it helps people finish the task instead of merely reporting that it broke.
