Command Palette

Search for a command to run...

Hectal
Course outline
Topic 10.4Intermediate ~18 min · step 4 of 7

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.

0/7 · 0%

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; polite waits 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. 1

    Give the slow load enough time

    In the deadline example change 800 to 4000 and watch the console: after about 3 seconds the slow load prints slow load: the bike, because it now finishes before the deadline.

  2. 2

    Hear the status

    With a screen reader running (NVDA on Windows, VoiceOver on a Mac with Cmd+F5), run the first example. Because #status has role="status", you hear each message as it changes, without the focus moving.

Code & diagrams

Loading, error and Try again New tab

The first load fails on purpose; the example then presses Try again itself.

idle

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 -> ready
A deadline for a load that hangs New tab

The 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 button

Break 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) {}.

terminal
$ Run
── what you'll see ──
on screen: "Loading the bike…" | retry button visible: false
after retry: the bike is here
states: loading -> loading -> loading -> ready
The visitor sees "Loading the bike…" for ever after the failed load, and has no button to press. (Here the example presses the hidden button by code, so the retry still happens.)

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 fetch with an AbortController: download the bytes yourself (fetch(url, { signal })), then loader.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. 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. 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. 3

    Progress is not guaranteed. The browser reports progress from the response's size, which needs a Content-Length header 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. 4

    A deadline. A load can hang: a stalled connection, a server that never answers. Promise.race([load, deadline]) with a setTimeout that 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. 5

    Accessibility. Put the status in an element with role="status" and aria-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

01

Why can a loading bar not always show a percentage, and what should it show instead?

02

What should the visitor see, and what should the developer see, when a model fails to load?

Practice

01

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."

02

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