Skip to content

Intermediate · Connecting frontend and backend · topic 19 of 34 · 30 min

Loading, errors and slow networks

Explanation

Every request can be slow or fail, so each screen tracks three things: loading, error and data. A network error (fetch throws) is different from an HTTP error (the server answered 4xx or 5xx). Add timeouts, cancel requests the user no longer needs, and retry only safe requests. Never send a payment or a new note twice by accident.

Three pieces of state

While a request is on its way, the screen must know three things: is it loading, did it fail (error), and what came back (data). In React each one is a piece of state. try/catch/finally fits this well: try does the request, catch stores the error, and finally runs in both cases, so it is the right place to turn loading off.

javascript
function NotesList() {
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    async function load() {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch('/api/notes', { signal: controller.signal });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const data = await res.json();
        setNotes(data.notes);
      } catch (err) {
        if (err.name !== 'AbortError') setError(err);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }
    load();
    return () => controller.abort(); // leaving the page cancels the request
  }, []);

  if (loading) return <p>Loading notes...</p>;
  if (error) return <ErrorBanner error={error} />;
  return <NoteGrid notes={notes} />;
}
Loading, error and data in one component. The cleanup function aborts the request when the user leaves the page, and an aborted request is not shown as an error.
Think of it like this: Booking a cab on a phone app works like this. First you see "Finding your driver" (loading). If nobody accepts in a few minutes, the app says "No cabs nearby, try again" (a timeout with a friendly message). If you press Cancel, the app stops searching at once (abort). And if your mobile data drops, it says "No internet", which is a different problem from "no cabs".

Two kinds of failure

Network errorHTTP error
What happensfetch rejects (throws), often TypeError: Failed to fetchfetch resolves, but res.ok is false
Typical causesNo internet, server down, CORS blocked, timeout400 bad input, 401 not logged in, 404, 500 server bug
Is there a body?No answer at allYes, usually { error: { code, message } }
What to tell the user"You seem to be offline" or "Taking too long"The server's message, or a friendly text for the status

Timeouts and cancelling

fetch has no short timeout of its own; on a weak signal a request can hang for a long time. An AbortController is an object with a signal you pass to fetch; calling controller.abort() cancels the request, and fetch rejects with an error named AbortError. AbortSignal.timeout(ms) is a ready-made signal that cancels itself after ms milliseconds; its error is named TimeoutError.

javascript
try {
  const res = await fetch('/api/notes', { signal: AbortSignal.timeout(8000) });
  // ... check res.ok and read the JSON
} catch (err) {
  if (err.name === 'TimeoutError') showMessage('The server is taking too long. Please try again.');
  else showMessage('You seem to be offline. Check your internet and try again.');
}
Give up after 8 seconds instead of spinning forever.

Retry, but only when it is safe

Some failures are temporary: a network blip, or a 502, 503 or 504 while the server restarts. Retrying a GET is safe, because reading the notes twice changes nothing. Retrying a 4xx is pointless: the request itself is wrong, so the answer will be the same. Wait longer before each new try (500 ms, then 1 s, then 2 s), which is called exponential backoff, so a struggling server is not flooded.

Careful: Never retry a POST automatically when you do not know if it reached the server. If the server saved the note (or took the money) and only the answer was lost, a retry makes a second note, or a double UPI payment. Payment systems use an idempotency key: a unique id the client creates once per action and sends with every try. The server remembers it and ignores repeats.
javascript
// Create the key ONCE when the user presses Pay, and reuse it for any retry
const key = crypto.randomUUID();
await fetch('/api/payments', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
  body: JSON.stringify({ amount: 499, upiId: 'asha@okbank' }),
});
If the same key arrives twice, the server returns the first result instead of charging again.

Friendly messages and double submits

SituationMessage to show
Network errorYou seem to be offline. Check your internet and try again.
TimeoutThe server is taking too long. Please try again.
400 with a messageThe server's message, such as "Title is required", next to the field
401Please log in again. (then go to the login page)
404This note was not found. It may have been deleted.
500 to 599Something went wrong on our side. Please try again.

A double submit happens when an impatient user taps Save twice on a slow network and two notes are created. Disable the Save button while saving, change its text to "Saving...", and enable it again only when the answer (or an error) arrives.

Tip: Test the bad cases on purpose. In Chrome DevTools, the Network tab has a throttling menu with "Slow 4G" and "Offline". Stop your Express server too, and check that every screen shows a clear message instead of a blank page.

Watch the lesson

Loading, errors and slow networks · 1/7

Loading Error Data

Real networks are slow and sometimes fail. Every screen must handle that, not just the happy path.

Prefer a video by a teacher? Find free videos on “react fetch loading error state abortcontroller retry tutorial” on YouTube ↗

In real life: Booking a train ticket at a busy hour

At the busy morning booking hour, a train booking site is very slow. A good app shows a clear "Please wait, do not press back" message instead of a frozen screen, and the Book button is disabled after the first tap. Search results (GET) can be retried safely. The booking itself (POST) is not retried blindly: if the answer is lost, the app checks the booking status with the request id it already has, so the passenger is not charged twice. A UPI app does the same with a pending payment: it shows "Payment pending" and checks the status later rather than paying again.

Practice (0/3 done)

Task 1 · Code · Javascript

Write friendlyMessage(problem). problem is either an HTTP status number or an Error object. Rules, in this order: an Error whose name is "TimeoutError" gives "The server is taking too long. Please try again."; any other Error gives "You seem to be offline. Check your internet and try again."; 401 gives "Please log in again."; 404 gives "This note was not found."; 429 gives "Too many tries. Please wait a minute."; 500 to 599 gives "Something went wrong on our side. Please try again."; anything else gives "Something went wrong."

Task 2 · Code · Javascript

Write async fetchWithRetry(fn, retries, wait). fn() makes one request: it returns a response-like object { ok, status } or throws on a network error. Call fn(). If it throws, or the status is 500 or more, try again, up to retries more times. Before retry number n (1, 2, 3...) call await wait(100 * 2 ** (n - 1)), so the waits are 100, 200, 400 ms (exponential backoff). Return the first response with a status below 500 at once (never retry a 4xx). If every try fails, return the last response if the last try gave one, or throw the last error if it threw. (wait is passed in so the tests do not need real timers.)

Task 3 · Practice

Make the Notes list and the note form in your React app ready for slow and failing networks. Then test with DevTools throttling ("Slow 4G" and "Offline") and with the Express server stopped.

Quick quiz

Question 1

1.Where should setLoading(false) go so it runs after both success and failure?

Question 2

2.The phone has no internet. What does await fetch("/api/notes") do?

Question 3

3.Which request is safe to retry automatically after a network error?

Question 4

4.What is an idempotency key?

Question 5

5.How do you cancel the notes request when the user leaves the page in React?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: CORS, origins and cookies