Loading, Empty, Error, and Success States
Every piece of asynchronous UI has at least four states. Designing for all four from the start — not just the happy path — is what separates a real component from a demo.
What you'll learn
- Name the four states any data-driven UI needs to handle explicitly
- Derive the current UI state from raw data rather than tracking it separately
- Explain why an empty state is not the same as an error state
Prerequisites
Explanation
A tutorial's version of a course list renders the courses. A real one has to answer at least four questions before it can render anything at all: is the data still loading? did loading fail? did loading succeed but return nothing? or did it succeed with real data to show? Skipping any of the first three means real users hit a blank screen, a frozen spinner, or a wall of undefined values — not hypothetically, but the very first time their network is slow or a filter matches nothing.
The tempting-but-wrong approach is tracking each of these as its own separate boolean: isLoading, isError, isEmpty, hasData — four independent flags that can drift out of sync with each other (what does the UI do if isLoading and isError are somehow both true at once? That state shouldn't exist, but nothing prevents it). The more robust approach derives the current state from the actual data, computed fresh every render, so an invalid combination is structurally impossible rather than merely unlikely:
function deriveUiState({ isLoading, error, items }) {
if (isLoading) return "loading";
if (error) return "error";
if (items.length === 0) return "empty";
return "success";
}
One value, always consistent with the underlying data, checked in one place, rendered with one switch or chain of conditionals. An empty state and an error state are not the same thing and should never share UI. "No courses match your filters — try clearing them" is helpful and expected. "Something went wrong loading courses — try again" is a failure. Showing the error UI for a legitimately empty result (or the empty-state UI when the request actually failed) both mislead the user about what's actually true and what they should do about it.
Example
Deriving one clean state value from raw data, instead of juggling independent booleans that could contradict each other.
function deriveUiState({ isLoading, error, items }) {
if (isLoading) return "loading";
if (error) return "error";
if (items.length === 0) return "empty";
return "success";
}
console.log(deriveUiState({ isLoading: true, error: null, items: [] })); // "loading"
console.log(deriveUiState({ isLoading: false, error: "Network error", items: [] })); // "error"
console.log(deriveUiState({ isLoading: false, error: null, items: [] })); // "empty"
console.log(deriveUiState({ isLoading: false, error: null, items: [1, 2] })); // "success"Try it yourself
Call deriveUiState with isLoading AND error both truthy — which one should 'win'? Check the function's actual order of checks.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Using deriveUiState already defined, classify three scenarios by calling it: scenarioA (a search that legitimately matched zero results, not loading, no error), scenarioB (the request is still in flight), scenarioC (the request failed with a network error). Store each result.
Checks: correctly derives the empty state · correctly derives the loading state · correctly derives the error state
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write a function stateMessage(state) that maps each of the four states ('loading', 'error', 'empty', 'success') to a distinct, user-facing message string. Any other input should return 'Unknown state'.
Checks: loading has a real message · all four states have distinct messages · an unknown state falls back correctly
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Tracking isLoading, isError, and isEmpty as independent booleans instead of deriving one consistent state value, allowing impossible combinations to accidentally occur.
- Rendering the error UI for a legitimately empty result, or the empty-state UI when the request actually failed — misleading the user about what's actually true.
- Forgetting the loading state entirely and letting the UI flash blank or stale content while a request is in flight.
Knowledge check
Takeaway
Real data-driven UI handles four states — loading, error, empty, success — derived from the actual data in one place, rather than tracked as independent booleans that can silently contradict each other.
Summary
This lesson covered deriving a single consistent UI state from raw data and explained why the empty and error states specifically must never share UI, since they mean genuinely different things to the user.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.