Loading and error states

Show the person exactly what is happening with a request.

What to build

One load(url) function that handles a success, an error response and a network failure.

Requirements

  • While loading: #status says Loading..., and all three buttons are disabled.
  • On success: list the products in #products, as NAME: PRICE, and set #status to Loaded N products. Loading again must not duplicate the list.
  • If the response is not OK: empty the list and set #status to Could not load (STATUS).
  • If the request itself fails, so that the promise rejects: empty the list and set #status to Could not reach the server.
  • The buttons are always turned back on afterwards.

Constraints

Do not change the first script, which stands in for a server.

Example

The button #bad requests /api/missing, which gives a status of 404, so #status shows Could not load (404).

Why it works

The two failures reach you differently: one as a normal response with ok set to false, and the other as a rejected promise. Treating them separately lets the message be accurate, and finally handles the part that is the same for all outcomes.

Hints

Hint 1

Write one load(url) function and call it from all three buttons.

Hint 2

Inside it, check response.ok and throw an Error that carries the status. Keep the network failure separate.

Hint 3

Use finally to turn the buttons back on.

Solution

Show one correct solution (try it yourself first)
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Loading states</title>
</head>
<body>
  <button id="good">Load products</button>
  <button id="bad">Load a missing page</button>
  <button id="offline">Try while offline</button>
  <p id="status" role="status">Nothing loaded yet.</p>
  <ul id="products"></ul>
  <script>
    // A stand-in for a real server, because this page has no network.
    function fakeFetch(url) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (url === "/api/offline") {
            reject(new TypeError("Failed to fetch"));
          } else if (url === "/api/products") {
            resolve({ ok: true, status: 200, json: async () => [{ name: "Pen", price: 10 }, { name: "Ruler", price: 25 }] });
          } else {
            resolve({ ok: false, status: 404, json: async () => ({}) });
          }
        }, 40);
      });
    }
  </script>
  <script>
    const status = document.querySelector("#status");
    const list = document.querySelector("#products");
    const buttons = document.querySelectorAll("button");

    function setBusy(busy) {
      for (const button of buttons) {
        button.disabled = busy;
      }
    }

    async function load(url) {
      status.textContent = "Loading...";
      setBusy(true);
      list.replaceChildren();
      try {
        const response = await fakeFetch(url);
        if (!response.ok) {
          status.textContent = "Could not load (" + response.status + ")";
          return;
        }
        const products = await response.json();
        for (const product of products) {
          const li = document.createElement("li");
          li.textContent = product.name + ": " + product.price;
          list.append(li);
        }
        status.textContent = "Loaded " + products.length + " products";
      } catch (err) {
        status.textContent = "Could not reach the server";
      } finally {
        setBusy(false);
      }
    }

    document.querySelector("#good").addEventListener("click", () => load("/api/products"));
    document.querySelector("#bad").addEventListener("click", () => load("/api/missing"));
    document.querySelector("#offline").addEventListener("click", () => load("/api/offline"));
  </script>
</body>
</html>

Lesson: Fetching data and saving it →