A mockup usually shows the best possible moment: the data arrived, the list has just the right number of items, and every image loaded. Real interfaces spend a surprising amount of time somewhere else.
Loading, empty, and error states are part of the feature. Writing them down early changes the way you build it.
Loading should explain the wait
Choose a loading treatment that fits the action. A full-page placeholder may make sense on the first visit. It is usually excessive when someone refreshes a list they can already read.
Keep the layout stable where possible. Reserve space for a cover image or an article title so the page does not jump as data arrives. A short label can explain an action better than a decorative spinner alone.
For an action button, show that the action started and prevent accidental repeated submissions when repetition would be a problem. Decide what happens if the reader leaves the page before the request finishes.
Empty is a result, not an error
There are several different reasons a list can be empty:
- The account is new and nothing has been created.
- A search found no matches.
- A filter excluded all existing results.
- The user has completed every item.
These situations need different copy and actions. “No results” is less useful than “No articles match this search. Try another phrase or clear the topic filter.”
An empty state should point toward a meaningful next step. Avoid offering a Create button when the person does not have permission to create anything.
Errors need a path forward
A good error message answers three questions: what did not happen, what still exists, and what can happen next?
Consider an article editor. “Something went wrong” leaves the writer guessing whether their work is gone. A more useful message is: “The post could not be saved. Your text is still here. Try again when your connection returns.” Only make that promise if the application actually preserves the text.
Different failures need different recovery:
| Situation | Useful response |
|---|---|
| Network unavailable | Preserve local work and offer retry |
| Session expired | Ask the person to sign in again |
| Invalid input | Identify the field and explain the correction |
| Permission denied | Explain the access limitation |
| Item removed | Offer navigation to an available page |
Avoid showing internal stack traces or service credentials in public error messages. Keep diagnostic detail in the appropriate logs.
Write the state table first
Before implementing a data-heavy screen, sketch the combinations that matter:
No previous data + pending request → initial loading
Previous data + pending request → refresh indicator
Successful request + empty result → empty state
Successful request + items → content
Failed request + previous data → content with warning
Failed request + no previous data → error with recovery
That table exposes decisions a single isLoading boolean cannot express. It also gives a reviewer and a tester a shared vocabulary.
Verify the uncomfortable moments
Try a slow connection, an empty response, a failed request, and a rapid change of filters. Navigate away while something is loading. Check that an older request cannot overwrite a more recent result.
Read status messages with a screen reader and test buttons with the keyboard. A state that only exists as a color change is easy to miss.
The feature is ready when its difficult moments are as understandable as its best one.