Course outline
Loading, Empty and Error States
In one line
A model takes time to arrive and sometimes never does. The page must always say what is happening: loading (with progress when it can), an error in plain words with a Try again button, and a deadline for loads that hang.
Think of it like this
A good restaurant tells you when your order is coming, and if the kitchen runs out, the waiter comes back and offers something else. A bad one leaves you staring at an empty table. Lena's black screen is the empty table.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- loading state
- What the page shows while something is on its way.
- error state
- What the page shows after something failed, ideally with a way to try again.
- aria-live
- An attribute that asks screen readers to announce changes to an element's text;
politewaits until the reader is idle. - Content-Length
- A response header with the size of the file in bytes; without it, progress cannot be a percentage.
Step by step
01Four states in one element
The first example's show(state, text, retry) writes the state into #status: the text, whether Try again is visible, and whether the whole status is hidden (when ready). Every state change goes through this one function.
02Fail, then recover
The first load uses a misspelled file name on purpose: the status shows the error text and the button, and the console keeps the real 404 for the developer. Then the example clicks Try again for you: the second load uses the right name and the bike arrives. The last line lists every state: loading -> error -> loading -> loading -> ready (the second "loading" is the progress update at 100%).
03A deadline
The second example races a fast and a slow stand-in load against an 800 ms deadline. The fast one wins; the slow one (3 s) loses and becomes an error message. Real loads use the same withTimeout().
04Choose the words
The visitor's message says what happened and what to do, with no codes: "The bike could not be loaded. Check your connection, then try again." The error object goes to console.error for you. Never show TypeError: Failed to fetch to a customer.
Try it yourself
- 1
Give the slow load enough time
In the deadline example change
800to4000and watch the console: after about 3 seconds the slow load printsslow load: the bike, because it now finishes before the deadline. - 2
Hear the status
With a screen reader running (NVDA on Windows, VoiceOver on a Mac with Cmd+F5), run the first example. Because
#statushasrole="status", you hear each message as it changes, without the focus moving.
Code & diagrams
The first load fails on purpose; the example then presses Try again itself.
Console
Run the scene to see its console.Expected console output
error kept for the developer: fetch for "/data/3d/models/lumen-bik.glb" responded with 404: Not Found
on screen: "The bike could not be loaded. Check your connection and try again." | retry button visible: true
after retry: the bike is here
states: loading -> error -> loading -> loading -> readyThe slow "load" is a stand-in that takes 3 seconds; the deadline is 800 ms.
Console
(nothing was printed)Expected console output
fast load: the bike
slow load: timed out after 800 ms -> show the still picture and a Try again buttonBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Swallow the error
In the first example replace the whole catch (error) { ... } block with catch (error) {}.
Myth vs fact
Myth
Fast connections don't need loading states.
Fact
Loading states are for the slow and failed cases, which every site has: phones on trains, hotel Wi-Fi, a server having a bad minute. They cost nothing when things are fast.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A real cancel needs
fetchwith anAbortController: download the bytes yourself (fetch(url, { signal })), thenloader.parseAsync(buffer, path). Then a timeout or a "Cancel" button really stops the download. - ▸
For a 3D hero on a marketing page, show a still image (the poster from Phase 1) at once and fade to the 3D scene when it is ready. The page looks finished immediately and the largest paint is the image (Topic 18.4).
Remember this
- 1
The problem this solves: loading in the browser is silent. Without a message, a slow phone looks broken, and a failed load looks like a slow one for ever. Visitors leave either way. Showing the state costs a few lines and changes how the product feels.
- 2
Four states. Loading: say so at once, and show a percentage when the size is known (
event.lengthComputable). Ready: hide the message. Error: plain words for the visitor ("The bike could not be loaded. Check your connection, then try again.") and a Try again button; the technical message goes to the console for you. Empty: when there is nothing to show (a part with no model yet), say that, rather than an empty canvas. - 3
Progress is not guaranteed. The browser reports progress from the response's size, which needs a
Content-Lengthheader and arrives in chunks. On this page the whole bike arrived in one progress event (1,325,548 of 1,325,548): a fast local server sends it in one piece. On a slow phone you get many. Some servers send no length (compressed on the fly), and then you can only say "Loading…". - 4
A deadline. A load can hang: a stalled connection, a server that never answers.
Promise.race([load, deadline])with asetTimeoutthat rejects turns "forever" into an error you can handle. The loader keeps downloading in the background (three.js loaders cannot be cancelled), but your page moves on. - 5
Accessibility. Put the status in an element with
role="status"andaria-live="polite": screen readers then read each new message without moving focus. The Try again button must be a real<button>that a keyboard can reach. Ravi, who tests with a screen reader, hears "Loading the bike… 40%" and then "The bike could not be loaded…".
Explain it without notes
Why can a loading bar not always show a percentage, and what should it show instead?
What should the visitor see, and what should the developer see, when a model fails to load?
Practice
Add an "empty" state to the first example's show() for a part that has no model yet, with the text "No 3D model for this part yet."
Write withTimeout() so it also clears its timer when the load wins.
Done when you can
Show loading, ready, error and empty states for a model
Report progress only when the size is known
Put a deadline on a load with Promise.race
Make the status heard by screen readers and the retry reachable by keyboard