Stop 09 · Connecting Frontend & BackendTopics · 5/8

Loading & Error States

Part of the Full Stack Developer Roadmap

In short

A blank screen for three seconds feels broken, even if the data is on its way. Clear loading, empty and error screens are what make an app feel trustworthy.

Designing what your UI shows while waiting for the backend, and when something goes wrong — the difference between an app that feels broken and one that feels polished.

The blob character waits on a railway platform, looking up at a loudspeaker as a train approaches.

Think of it like this

Platform announcements

A good station tells you 'train arriving on platform 3', 'running 20 minutes late' or 'cancelled, take the next one'. A bad station says nothing and you stare at the tracks. Your UI is the station: announce every state and never leave the user guessing.

Watch it happen

One request, every screen

Idle
Loading
Success
Error
Retry

1/6Nothing has been asked yet. A search box might be waiting for input.

The big ideas

What you need to know

Model the states explicitly

Use one status value: 'loading', 'error' or 'success'. Separate isLoading and isError booleans can contradict each other.

Empty is not an error

Zero results is a valid answer. Show 'No orders yet' with a next step, not a red error box.

Errors in human words

Show 'Couldn't load orders. Check your connection.' Keep stack traces in the console and your logs.

Avoid spinner flashes

For quick responses, a skeleton shaped like the final layout feels smoother than a spinner that flickers for 100 ms.

See it in code

Walk through the code

tsx
1type State<T> =2  | { status: "loading" }3  | { status: "error"; message: string }4  | { status: "success"; data: T };5 6function Orders({ state, retry }: { state: State<Order[]>; retry: () => void }) {7  if (state.status === "loading") return <OrdersSkeleton />;8  if (state.status === "error")9    return (10      <p role="alert">11        Couldn't load orders. <button onClick={retry}>Try again</button>12      </p>13    );14  if (state.data.length === 0) return <p>No orders yet. Your first one is a click away.</p>;15  return <ul>{state.data.map((o) => <li key={o.id}>{o.item}</li>)}</ul>;16}

1/4Exactly one state at a time. Impossible combos like 'loading and error' can't be written.

With a union type, TypeScript won't let you read state.data until you've checked the status is 'success'.

Save yourself the pain

Common mistakes

Oops

Showing a spinner forever when the request fails.

Do this instead

Always end in success or error. Catch failures, add a timeout, and offer a retry button.

Oops

Treating an empty list as an error.

Do this instead

Empty is success with no data. Show a friendly empty state that tells the user what to do next.

Your mission

Give every state a screen

~25 min

Quick check

Your API returns 200 with an empty array. Which screen should the user see?

Go deeper

  1. 1Add explicit loading, error and empty states to every data-fetching component.
  2. 2Build a reusable skeleton loader component reused across your app.
  3. 3Design user-friendly error messages instead of showing raw error objects.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge