Loading, empty, error: the three screens that show a product's manners
Loading, empty, and error states are not edge cases: they are what users meet on slow networks, new accounts, and bad days. This guide covers skeleton and progress rules, four distinct kinds of empty, error copy that protects user input, race conditions in state transitions, and an acceptance checklist.

Every screen in your project lives in one of three states, whether you designed them or not: it is still loading, it has nothing to show, or something went wrong. Loading, empty, and error states are not edge cases. They are the states users meet most often on slow networks, on new accounts, and on bad days. Most vibe projects spend weeks polishing the "everything worked" screen and leave these three states to defaults: a blank page, a spinner that never stops, or a raw error dump. This guide fixes that, one state at a time, with copy you can steal and a checklist you can run before shipping.
Start with one principle that governs all three states: never leave the user guessing about what is happening, what happened, or what to do next. A loading screen should answer "is it working, and should I wait?" An empty screen should answer "is there really nothing here, or did something fail?" An error screen should answer "what broke, what did I lose, and how do I recover?" If a state answers none of those questions, it is not a design, it is an absence of one.
Loading: show progress, not patience tests
The first rule of loading states is about duration, not animation. If loading finishes in under about 300 milliseconds, show nothing at all; a spinner that flashes for a fraction of a second reads as a glitch, not as feedback. Between roughly 300 milliseconds and a few seconds, show a skeleton screen shaped like the content that is coming: gray blocks where text lines will be, a rectangle where the image will land. A skeleton works because it sets the user's expectation about layout, so the page does not jump when real content arrives. Beyond a few seconds, a skeleton is no longer enough: give the user a progress indication, a cancel option, or at least an honest message about what is taking so long.
Two loading mistakes show up constantly in vibe projects. The first is the full-page white screen with a centered spinner: it tells the user nothing about what is loading, and it hides navigation the user could otherwise use to go elsewhere. Prefer partial loading: keep the page chrome, navigation, and already-loaded sections visible, and load only the missing piece. The second is the fake progress bar, the one that crawls to 90% and then sits there. Users learn to distrust it within one session, and distrust transfers to everything else your product says. If you cannot measure real progress, use an indeterminate indicator honestly, and say what is happening in words: "Fetching your projects" beats a silent spinner every time.
Pagination and lists deserve their own loading rule: when the user loads more items, append a skeleton row at the end of the existing list rather than replacing the whole list with a spinner. And when filters change, clear the old results only once the new request starts returning, or at minimum show that the list is refreshing; flashing stale results for a second after the user changed a filter is how users end up clicking the wrong item.
Empty: distinguish four kinds of "nothing"
The word "empty" hides four different situations, and each needs a different screen. First, genuinely empty: a new user with no projects yet. Second, filtered to nothing: the user searched or filtered and nothing matched. Third, not loaded yet masquerading as empty: the request failed silently and the screen looks like an empty list. Fourth, no permission: the user cannot see items that exist for someone else. Showing the same "No data" illustration for all four is how users end up confused, or worse, believing their data is gone when it is only filtered out.
A good empty screen has three parts: a plain-language statement of the situation, a reason if there is one, and one next action. For a new user, that action is creation: "You haven't created a project yet" plus a "Create your first project" button, and ideally one sentence about what a project is for. For a filtered-to-nothing result, the action is adjustment: "No projects match 'invoice'" plus "Clear filters" and a suggestion to try a broader term. For a permission case, say so directly: "You don't have access to this workspace's projects" plus who to ask, rather than pretending nothing exists. Never use an empty state to deliver a lecture about features, and never leave it without an action: an empty screen with no next step is a dead end with nice illustration.
One more empty-state trap: the celebratory illustration that celebrates nothing. A big confetti graphic on a screen that says "No orders yet" reads as tone-deaf to a seller waiting for their first sale. Match the tone to the situation: new-user emptiness can be encouraging, filtered emptiness should be neutral and helpful, and error-related emptiness should not celebrate at all.
Error: say what broke, protect what the user typed, offer a way back
Error screens have one job above all: do not make the user lose work. If a form submission fails, keep every field the user filled in, show the error next to the action that failed, and let the user retry without retyping. Losing a long form to a network error is the kind of experience users remember and tell other people about. Autosave drafts for anything longer than a few fields, and retry automatically once for transient network failures before showing an error at all.
Error copy follows a simple structure: what happened in plain language, what it means for the user, and what to do next. "Something went wrong" fails all three parts. "We couldn't save your project because the connection dropped. Your changes are still here. Try saving again" passes all three. Keep a short error code visible, but small and secondary: "Error code 503" helps support help you, but it should never be the headline. And distinguish retryable errors from terminal ones: a network timeout deserves a retry button; a permission denial deserves an explanation and a contact, not a retry button that will fail identically forever.
Errors also need a hierarchy. A failed background sync can be a quiet banner, not a full-page takeover; a failed payment or a failed publish deserves the full screen, because the user cannot safely proceed without resolving it. If every error is a modal that blocks the whole page, users learn to dismiss errors without reading them, which is the worst possible outcome for the one error that actually matters.
The transitions between states, where most bugs live
The individual states are the easy part. The bugs live in the transitions: the old request that arrives late and overwrites newer results, the filter change that briefly shows yesterday's list, the double submit that creates two records, the error that appears and then vanishes when a stale success arrives a second later. These are race conditions, and they are worth a dedicated test pass because users experience them as "the app showed me the wrong thing," which erodes trust faster than any single error message.
Three defenses cover most of it. First, tag each request with the state it belongs to, and ignore responses that arrive after the user has moved on: a search for "apple" should never overwrite results for "apricot" just because its response arrived later. Second, disable the submit button after the first click and re-enable it on failure, so a double tap cannot create a duplicate. Third, when a request fails after a previous success, keep showing the last good data with a "last updated" note and a retry affordance, rather than replacing good data with an error screen. Stale data with a warning is almost always better than no data at all.
Acceptance checklist
Before shipping a feature, run through these states on a real device with a throttled network. Loading: does anything flash for under 300 milliseconds? Does a skeleton match the layout that follows? Does a slow load offer cancellation or an explanation? Empty: can you tell a new user, a filtered result, a permission denial, and a failed load apart? Does each one offer exactly one clear next action? Error: does a failed submit preserve everything the user typed? Does the copy say what happened, what it means, and what to do next? Is there a visible error code for support, kept secondary? Transitions: change a filter quickly three times and check the final list matches the final filter; double-tap submit and check only one record exists; kill the network mid-load and check the screen degrades to a retry state instead of a blank page.
Loading, empty, and error states are where your product's manners show. The happy path shows what your product can do; these three states show how it treats people when things are slow, new, or broken. Users forgive slow and broken far more readily than they forgive being left guessing, and the difference between the two is usually a few lines of honest copy and one clear next action.

Comments (0)
Related articles

A desktop-first web project is not mobile-usable just because it displays on a phone. This guide works through core-flow priorities, thumb zones, layout degradation for tables and charts, input and keyboard pitfalls, navigation and performance budgets, and a real-device acceptance checklist.

When an agent says 'done,' you neither need to doubt it nor believe it — just open the checklist and ask, item by item, for the evidence, the pass bar, and the return rules. This guide gives an acceptance-first five-step method, what 'good' looks like for five kinds of evidence, a copy-ready acceptance matrix, and three worked examples.

'Improved the experience, fixed known issues' says nothing wrong and tells users nothing. This guide gives a five-section structure for three reader groups, good-versus-bad examples, a four-step workflow from code changes to notes, five common pitfalls, a ten-phrase ban list, and a copy-ready template.