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
fetchin 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)
- Idle / loading — request in flight
- Success — array to render
- 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
- Add a Retry button on error.
- Show a friendly empty state if the array is empty.
- 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.
- React Testing Library basics — carry your practice mindset into UI-level checks
- Diff Checker — compare your first and second solution to spot cleaner code