UI states: design loading, empty and error before the happy path

Your designs are three happy-path screens. The states a user actually meets on a bad day get invented in the last two days of the sprint, by whoever is holding the ticket.

The design you were given has three screens and they all have data in them. The list has eight rows, the form is filled in, the confirmation is green.

Then you build it, and within an hour you need to know what it looks like while it is loading, what it says when the list is empty, and what happens when the thing the user just clicked was taken by somebody else four seconds ago. None of that is in the design, so you decide it, at four in the afternoon, alone.

Which states get designed and which get improvised A screen has five states. The loaded one is designed in advance. Loading, empty, conflict and failure are usually decided at the end of the work, and they are the states a user meets when something has gone wrong for them. designed up front loaded, with data decided at the end, if at all loading nothing to show somebody beat you to it the request failed the four below the line are the ones a person sees on the day the system lets them down
A feature is not designed until the bad states are written down. They are the ones that get improvised, and improvised is how the same error ends up worded three ways. Illustrative — the shape of the argument, not measured data.

The four that get skipped are the four that matter

A screen has more states than it has designs. The ones that get drawn are the ones where everything worked, and the ones that get improvised are the ones a person meets on the day the system is letting them down.

That is exactly backwards. Nobody remembers a list that loaded. Everybody remembers being told “an error occurred” while the thing they wanted disappeared.

So the rule is small and blunt: a feature is not designed until its loading, empty, conflict and failure states are written down. Four extra lines in the document, decided when there is time to think, instead of at the end when there is not.

What the conflict state is really for

Of the four, the conflict is the one teams argue about, because it feels rare. It usually is not. Any system where people choose from a shared pool has a moment where two of them choose the same thing, and if your users mostly act at the same time of day, that moment is not an edge case, it is a Tuesday.

It is also the only state where the system says no to somebody who did nothing wrong. That deserves better than a generic message, and it is where a requirement comes from:

Scenario: somebody else took it first
  Given a desk that was free when the page loaded
  When a member books it and it has since been taken
  Then the board says that desk went, keeps the date they had chosen,
    and shows the list already updated

Read that back as three product decisions. The message is specific. The context the user built up is not thrown away. And the screen is usable again immediately, without a reload.

None of that survives being decided at four in the afternoon.

Where the wording belongs

One rule saves more argument than any other: failure wording belongs to the API, not to the screen.

The client renders the message it was sent. It does not invent one, and it does not translate one. Break that and the same failure is worded three ways in three clients, drifts apart, and the support team learns three vocabularies for one event.

It has a useful side effect. Once the wording belongs to the API, a contract test should be strict about the status and loose about the text, because the text is not part of the coupling. A design decision made about a screen turns out to decide what a test asserts, which is the sort of thing you only notice when both are written down.

The cheapest version of this

You do not need a design system or a new document type. You need a table, in whatever file already describes the feature, with one row per state and one sentence about what is on screen and why.

state what is on screen why
loading a status the screen reader announces a blank frame reads as broken
empty the filter, and an empty list hiding the filter removes the way back
conflict the API’s message, list refreshed they must be able to pick another
failed the API’s message never a message invented here

Writing that took four minutes. It settles arguments that otherwise happen twice, once during build and once during review, and it gives the person building the screen something to check against rather than something to invent. That is the same reason a rule you can execute beats a rule you can read.


If your team’s designs stop at the happy path, that’s the work I do.


Working through this in your own team?

I help engineering teams adopt AI coding assistants without giving up engineering rigour, and design the architecture underneath. If that's on your plate, let's talk.