checklistuiuxstatesqa

Screen states: empty, loading, error — and the ones people forget until a bug arrives

Almost every screen is drawn for one scenario — “all good, data is there.” The Figma mockup is pretty, the list is full, avatars in place. But the first thing a user sees is often something else: loading, emptiness, or an error. And those states usually weren’t designed by anyone — they come out however they come out.

Here’s a rundown of the screen states people forget until a bug lands on them. With examples, because I’ve stepped on almost every one of these myself.

The empty screen that looks like a stuck loader

A favorite. You open a section — a white screen. Is it still loading? Did it load but it’s empty? Or did it fail and stay silent? The user doesn’t know, and most decide “it’s broken” and leave. In reality everything works — the screen just has no “nothing here yet” state.

Hence the rule I start with: every screen with a list/data should have at least four drawn states — loading, has data, empty, error. If even one “comes out however it comes out,” that’s where the bug will be.

Loading: not a spinner for the spinner’s sake

Loading isn’t “a spinner in the middle and done.” What I check: does the layout jump when the data arrives (content shouldn’t shift and move a button under your finger — that’s layout shift, see CLS). Skeletons usually beat a spinner because they show the future structure and feel faster (NN/g on skeleton screens). And separately — what happens if loading hangs forever: is there a timeout that falls into an error, or an eternal spinner.

Two different empties people confuse

This matters, and it really does get confused. “Empty” comes in two kinds, and their messages differ:

  • First run / nothing yet — an empty cart, zero orders, a new account. Here emptiness is normal, and the screen shouldn’t apologize but prompt the first step: “Add your first item.”
  • Nothing found — there was a filter or search and nothing matched. This needs something else: say the criteria are too narrow and let them reset the filter. “Add your first item” here sounds mocking.

A good breakdown of empty states is at NN/g on empty states. In short: an empty screen isn’t “no content,” it’s a chance to tell the user what to do next.

”No data” and “couldn’t load” are NOT the same thing

The most common mistake I catch: on a network error they show the same thing as for an empty list. The user sees “nothing here yet,” thinks they really have nothing, and leaves — when in fact the request just failed.

These are different states with different actions. Empty → “add / change the filter.” Error → an honest “something went wrong” and a “Retry” button, not a white screen or an eternal spinner. Good error messages explain what happened and what to do, without “Error 500” to the face (Smashing on error messages).

The states you get to last

In my experience, things break here:

  • Offline / no network — separate from a server error: “no connection, check your internet,” ideally with auto-retry when the network is back.
  • Partial load — some data arrived, some failed. Show what you have and mark what didn’t load, instead of crashing the whole screen over one block.
  • Stale data — show the cache while fetching fresh, but honestly mark it as cache, otherwise the user decides on old numbers.
  • Error on load-more — the list loaded, but the next page failed on scroll: it must not wipe what’s already shown, and there should be a way to retry just the load-more.
  • Edge: exactly one item, exactly zero, very long content that breaks the empty/error layout.

Empty and error are accessibility too

This one gets forgotten entirely. An error message that appears dynamically must be announced by a screen reader — that’s what the alert role / aria-live is for (ARIA alert pattern). An error can’t be conveyed by color alone (a red border with no text tells a blind user nothing), and it’s sensible to move focus to the message or to “Retry.” An empty state should be readable too, not just an empty container.

Compact checklist

  • Every screen with data has 4 states: loading / has data / empty / error
  • Loading doesn’t jolt the layout (no layout shift); a timeout → error, not an eternal spinner
  • Empty distinguishes “nothing yet” from “nothing found” — different text and actions
  • Error ≠ empty: on failure, an honest message + “Retry,” not a white screen
  • Offline is separated from a server error; auto-retry when the network returns
  • Partial load doesn’t crash the whole screen; stale data is marked
  • An error on pagination/load-more doesn’t wipe what’s already shown
  • Edges: 0 / 1 / very many items, very long text
  • Error messages are announced to the screen reader (alert/aria-live), not color only
  • Retry actually works and clears the error state

In short

Everyone tests the happy path. Money and users’ nerves are lost on the other states — when empty looks like breakage, an error looks like emptiness, and loading never ends. I decided long ago: a screen isn’t done until I’ve looked at it with no network, with an empty response, and with a 500. That’s five minutes, and it catches what would otherwise come back from production.

Sources: NN/g — Empty states, Smashing — Error messages UX, NN/g — Skeleton screens, web.dev — Cumulative Layout Shift, W3C ARIA — Alert pattern.