Loading, Empty, and Error States

Most UI work focuses on the screen with data in it. Users spend a surprising amount of time on every other version of that screen: waiting for the first load, looking at a list with nothing in it yet, staring at a failed request, using slightly old data while a refresh happens, or opening the app on a train with no signal. These states decide whether an app feels solid or broken, and they are where accessibility, layout stability, and error handling are most often neglected.

A reliable approach is to treat every data-backed view as a small state machine and design each state deliberately — before the happy path is finished, not after the bug reports arrive.

TL;DR

Quick Example

A React list with TanStack Query that handles every state explicitly:

isPending is true only when there is no data at all; isFetching is true during any request, including background refreshes. Using the wrong one is how lists blank out every time a window regains focus.

Core Concepts

The States

Skeletons Versus Spinners

Skeletons — gray placeholders shaped like the content — reduce perceived wait and prevent layout shift because they reserve the final space. Spinners suit actions and small regions. For fast responses, delay any loading indicator by roughly 200–300 ms, so users on fast connections do not see a flash; once shown, keep it visible briefly to avoid flicker.

Suspense and Error Boundaries

In React, Suspense boundaries show fallbacks while children load, and error boundaries catch rendering and data errors for a subtree. Placing boundaries around independent regions lets one failing widget show its own error while the rest of the page works. See React Suspense.

Errors Worth Distinguishing

Accessibility

Layout Stability

Loading states are a major source of Cumulative Layout Shift. Reserve space with skeletons sized like real content, set dimensions on images and embeds, and avoid inserting banners above existing content after load. See Core Web Vitals.

Best Practices

Design All States Up Front

Add loading, empty, and error states to designs and acceptance criteria. Storybook stories for each state make them reviewable and testable.

Keep Showing Data During Refreshes

Background refreshes, pagination, and filter changes should keep current data visible with a subtle indicator. Replacing content with a skeleton on every refetch feels broken.

Fail Small

Scope errors to the smallest region that failed. A failed recommendations widget should not replace the whole page with an error screen.

Test Each State

Mock slow, empty, and failing responses in component and end-to-end tests — for example with Mock Service Worker — so these paths do not regress unnoticed.

Common Mistakes

Treating Empty as Loading

Generic Error Messages

Spinners That Replace Content

FAQ

Should I use skeletons or spinners?

Skeletons for content regions whose layout you know, because they reduce perceived waiting and prevent layout shift. Spinners for actions in progress, small regions, and unknown layouts.

How do I avoid loading flashes on fast connections?

Delay showing loading indicators by a few hundred milliseconds, and once shown, keep them for a minimum duration. React transitions and placeholder data also keep previous content on screen during quick updates.

What should an empty state include?

What the area is for, why it is empty, and the most useful next action — such as creating the first item, importing data, or clearing filters. Distinguish first-use empty states from "no results" states.

How should errors be shown to users?

Close to where the failure happened, in plain language, with what the user can do next — retry, sign in again, contact an admin — while leaving working parts of the page usable.

How do I make loading states accessible?

Mark loading regions with aria-busy, announce important results or failures through polite live regions, manage focus after user-initiated errors, and respect reduced-motion preferences.

Related Topics

References