Error and Success States: Designing Them as State Machines
How to design error, success, partial success and retry states as explicit UI states, with better feedback and fewer impossible combinations.
Core Takeaway
Error and success states are not decorations added at the end of a flow. They are states with their own copy, actions and exit paths. Model them explicitly as idle, submitting, success, partial success, error and retry, decide which actions are allowed in each, and make sure only one terminal state can be visible at a time.
Why terminal states need their own model
Most forms and async actions are built around a loading flag and a pair of error and success booleans. That is enough until a request succeeds partially, a retry fails, or a user edits the form while the previous request is still running. A state machine fixes the structural problem: the component is in one named state at a time, and each transition is explicit. Loading cannot coexist with success because no such state exists.
The states worth naming
Start with the states users can actually see.
- Idle: ready for input, with no stale message from a previous attempt
- Submitting: input is locked or clearly marked as in progress
- Success: the action completed, with a next step or a way to dismiss
- Partial success: some items worked and others did not, with a clear list
- Error: what failed, whether the input was kept, and what to do next
- Retrying: a distinct state so the user does not trigger a duplicate request
Write the copy before the animation
The most important part of an error state is the sentence. Say what happened, why if you know, and what the person can do. Avoid blaming the user and avoid a generic 'Something went wrong' when you have a specific cause. Success copy should name the result, not just celebrate. A checkmark with no explanation leaves people unsure whether the data was saved.
Use motion to show the change, not to delay it
A short transition can show that a message replaced a form, or that a row moved into a success group. Keep it brief and do not make people wait for a celebration before they can continue. Confetti is a poor fit for a failed payment or a destructive action. Reserve high-energy motion for genuine, low-risk wins, and provide a reduced-motion version that changes instantly.
Design the exit path
Every terminal state needs somewhere to go. Success should offer the next action or a clear dismissal. Error should preserve the user's work and offer retry, edit or support. Partial success should let people retry only the failed items. If the only exit is a page refresh, the state is not finished.
Keep the states in one place
When error and success are drawn as separate screens, they drift. When they are states in the same component, copy, layout and motion stay consistent. A code-first state machine such as XState is a better fit when the logic belongs in application code. A visual runtime is a better fit when designers need to edit the states and motion without touching that code.
Key Principles to Remember
- Treat success, partial success and error as terminal states with their own copy and actions
- One named state at a time prevents loading and success from appearing together
- Name the result and the next step; do not rely on a checkmark alone
- Keep transitions short and reserve high-energy motion for low-risk wins
- Always give the user an exit: retry, edit, dismiss or continue
Frequently Asked Questions
Sources
- The Role of Animation and Motion in UX — Nielsen Norman Group
- Understanding SC 2.3.3: Animation from Interactions — W3C WAI
- Response Times: The 3 Important Limits — Nielsen Norman Group