Course progress0%
Course content
50 min

Fetch without Hiding Loading or Failure

Validate API responses and render distinct asynchronous states

By the end of this lesson

  • Validate API responses and render distinct asynchronous states
  • Keep the current draft intact when catalogue loading fails

From contract to adapter

Create catalogue-api.js. It should know HTTP and the catalogue contract. It should not query the DOM or change an order. This function accepts a signal for the next module and a fetch implementation for tests.

The new Set stores unique values. Start empty, check ids.has(item.id) before accepting a product, then record the ID with ids.add(item.id). This makes duplicate detection explicit. A Set stores values; the Map from the server lesson stores key/value pairs. The adapter uses both familiar validation and this one new collection operation.

export async function getProducts(url, { signal, fetchImpl = fetch } = {}) {
  const response = await fetchImpl(url, { signal });
  if (!response.ok) throw new Error(`Catalogue request failed (${response.status})`);
  const body = await response.json();
  if (!body || !Array.isArray(body.products)) throw new Error('Invalid catalogue');
  const ids = new Set();
  for (const item of body.products) {
    if (!item || typeof item.id !== 'string' || !item.id.trim() ||
        typeof item.name !== 'string' || !item.name.trim() ||
        !Number.isSafeInteger(item.unitPriceCents) || item.unitPriceCents < 0 ||
        ids.has(item.id)) {
      throw new Error('Invalid product');
    }
    ids.add(item.id);
  }
  return body.products;
}

A 503 does not automatically make fetch reject: check response.ok yourself. JSON parsing can also fail. Read MDN's Fetch guide to compare transport errors with HTTP errors.

Connect one load before adding concurrency

Write a loadCatalogue controller with this sequence:

  1. Set catalogue state to loading, clear old catalogue items and render.
  2. Await getProducts('/api/products').
  3. Store the products and select ready or empty based on length.
  4. On failure, set error with a user-facing retry message.
  5. Render the final state.

For this first increment, disable Reload while loading so a person cannot start overlapping loads. Keep the form usable. Add a catalogue container and a persistent role="status" element. Use textContent for product names. Show prices through the same presentation formatter as your quote.

Choosing a product may populate the first draft item's name and price, then mark the quote dirty. That remains a local preview: a later server must resolve an ID to its own price before accepting an order. Do not imply catalogue selection has reserved stock.

Failure is a state with an exit

A catch block that only logs an error leaves the interface spinning forever. Conversely, turning an error into [] misrepresents a failed request as an empty catalogue. Keep those results separate.

Use the lab URLs to check ready, empty, error and slow behavior. Stop the server after the page loads and trigger another request to test transport failure. Start it again and Retry: recovery must not require throwing away the draft.

Independent challenge

Add a temporary server mode returning duplicate product IDs or a missing price. Expect error, with no partially rendered catalogue. Remove that diagnostic mode after recording the result. Explain why status 200 was insufficient evidence and why the adapter rejected the entire list.

Lesson complete?

Your progress is saved on this device.