What you'll be able to do

By the end of this page you should be able to fetch a JSON list, show loading and error UI, and render results — closing the practice phase with a realistic async screen.

Previous post: mini project — todo DOM.

Who this is for

  • People who can fetch in the console but not in a full UI flow
  • Juniors who forget loading/error states
  • Anyone preparing to read React data-fetch examples later

You can skip the exact API if you already built this — redo with clearer state labels. This closes Phase 12.

UI states (name them)

  1. Idle / loading — request in flight
  2. Success — array to render
  3. Error — message + retry

Scenario A — slow network. Without loading text, the page looks broken.

Scenario B — 404 or CORS failure. Without error UI, you stare at a blank list.

Fetch + render sketch

async function loadPosts() {
  statusEl.textContent = "Loading...";
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
    if (!res.ok) throw new Error("HTTP " + res.status);
    const posts = await res.json();
    renderPosts(posts);
    statusEl.textContent = "";
  } catch (err) {
    statusEl.textContent = "Could not load: " + err.message;
  }
}

Scenario A — happy path. Five titles in a list.

Scenario B — offline. Catch shows a human message.

Use a public demo API only for practice; real apps need your own backend and auth story.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <button id="load">Load posts</button>
    <p id="status"></p>
    <ul id="list"></ul>
    <script>
      const statusEl = document.getElementById("status");
      const list = document.getElementById("list");

      function renderPosts(posts) {
        list.textContent = "";
        for (const post of posts) {
          const li = document.createElement("li");
          li.textContent = post.title;
          list.appendChild(li);
        }
      }

      async function loadPosts() {
        statusEl.textContent = "Loading...";
        list.textContent = "";
        try {
          const res = await fetch(
            "https://jsonplaceholder.typicode.com/posts?_limit=5",
          );
          if (!res.ok) throw new Error("HTTP " + res.status);
          const posts = await res.json();
          renderPosts(posts);
          statusEl.textContent = "Loaded " + posts.length + " posts";
        } catch (err) {
          statusEl.textContent = "Could not load: " + err.message;
        }
      }

      document.getElementById("load").addEventListener("click", loadPosts);
    </script>
  </body>
</html>

Stretch goals

Scenario A — disable the button while loading.

Scenario B — client filter on titles after success (reuse array methods).

Mistakes I see a lot

1. No res.ok check — treating HTML error pages as JSON.

2. Nested .then pyramids when async/await would read clearer (you already practiced await).

3. Putting secrets in the frontend fetch to a privileged API.

4. Rendering without clearing the previous list.

What to try before the next post

  1. Add a Retry button on error.
  2. Show a friendly empty state if the array is empty.
  3. Compare this flow to your todo project's render habit.

This closes Phase 12 — practice. Next we begin Phase 13 — bridge: JavaScript to Node.js — what changes.

Try this next outside the series

Practice goes further when you test the result and compare revisions instead of solving once and forgetting.