Course: JavaScript Intermediate
Lesson 18: Working with API Data
Level
Intermediate
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Parse and render API response data into the DOM
- Handle missing or unexpected fields in API responses defensively
- Manage loading, success, and error states when fetching data
Frontend Development Context
Fetching data is only half the job — you also have to work with whatever comes back. Real APIs often return objects with more fields than you need, fields that might be null or missing, or arrays that might be empty. Writing code that handles all of these cases gracefully is what separates production-quality frontend code from fragile prototypes.
Managing loading, success, and error states is also a core skill. Users need feedback while data is loading, a clear display when it arrives, and a helpful message when something fails. This three-state pattern (loading / success / error) is how React, Vue, and most frontend frameworks approach async data rendering, but it is equally applicable in vanilla JavaScript.
Explanation
When an API responds with JSON, response.json() gives you a JavaScript value — usually an object or an array. From there, you access the fields you need and render them to the DOM. The challenge is that real APIs are not always consistent: a field that exists in most responses might be missing or null in some cases.
Defensive access patterns prevent crashes from missing data. Optional chaining (?.) lets you access nested properties without throwing if an intermediate value is null or undefined. Nullish coalescing (??) provides a fallback value when a property is null or undefined. Combining these two operators is the standard way to read API data safely.
Rendering an array of items means creating DOM elements in a loop — most often using forEach to build elements and append them to a container. Before rendering, clear the container's existing content so you do not append duplicates on re-renders.
Loading state management means tracking whether data is currently being fetched. Show a loading indicator before the fetch starts, remove it when the response arrives (or when an error occurs), and show the appropriate UI for the result. This three-state pattern makes the app feel responsive and trustworthy.
Empty state handling is also important: if the API returns an empty array, tell the user there is nothing to show rather than leaving a blank area with no explanation.
Code Example
This example fetches a list of posts, renders them with defensive field access, and manages loading, success, and error states.
async function renderPosts(userId) {
const container = document.querySelector("#postsContainer");
const status = document.querySelector("#status");
// Loading state
status.textContent = "Loading...";
container.innerHTML = "";
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const posts = await response.json();
// Empty state
if (posts.length === 0) {
status.textContent = "No posts found.";
return;
}
// Success state — render each post defensively
status.textContent = "";
posts.slice(0, 5).forEach(post => {
const title = post.title ?? "Untitled";
const body = post.body?.substring(0, 80) ?? "No content available";
const article = document.createElement("article");
article.innerHTML = `
<h3>${title}</h3>
<p>${body}…</p>
`;
container.appendChild(article);
});
} catch (error) {
// Error state
status.textContent = "Could not load posts. Please try again.";
console.error("renderPosts failed:", error.message);
}
}
renderPosts(1);
Code Explanation
The function starts by setting status.textContent = "Loading..." and clearing the container. This ensures the user sees feedback immediately and that stale content from a previous render is removed.
if (!response.ok) throw new Error(...) converts HTTP errors into exceptions that fall through to the catch block. This is the same pattern from Lesson 14 — essential for treating HTTP errors as failures.
posts.length === 0 handles the empty state explicitly. Rather than rendering nothing and leaving the user confused, the status message is updated to explain the result.
post.title ?? "Untitled" uses nullish coalescing to provide a fallback if title is null or undefined. post.body?.substring(0, 80) uses optional chaining to safely call substring — if body is null or undefined, the expression returns undefined instead of throwing a TypeError. The ?? "No content available" then catches that undefined.
The catch block handles all errors — network failures, HTTP errors, and any unexpected exceptions — by updating the status to a user-friendly message and logging the technical details.
Pattern Highlights
✅ Positive Pattern: Use
??for fallback values and?.for safe property access on data from external sources. API fields can be absent, null, or differently named than you expect.
⚠️ Neutral Note:
slice(0, 5)limits rendering to the first five results. In production, you would add pagination or infinite scroll, but limiting initial render prevents overwhelming the user with raw API data.
❌ Negative Pattern: Do not assume an API always returns the same shape. Fields that work in development against test data can be missing in production. Always write defensive access code for any field that might be absent.
Common Mistakes
Mistake 1: Not clearing the container before re-rendering
Appending items to a container that already has content from a previous render.
async function refresh() {
const posts = await loadPosts();
posts.forEach(post => {
container.appendChild(createCard(post)); // duplicates on every call!
});
}
Clear the container first: container.innerHTML = "". This prevents content from stacking up on each call.
Mistake 2: Accessing nested properties without optional chaining
Crashing when a nested field is null or missing.
const city = user.address.city; // TypeError if user.address is null
Use optional chaining: const city = user.address?.city ?? "Unknown". This prevents the crash and provides a readable fallback.
Mistake 3: Showing raw API error responses to users
Displaying technical HTTP status codes or stack traces in the UI.
catch (error) {
container.textContent = error.message; // "HTTP 500" or stack trace visible to users
}
Show friendly, actionable messages to users ("Could not load data — please try again"). Log the technical details to the console for developers. Keep these two concerns separate.
Quick Recap
- Use optional chaining (
?.) and nullish coalescing (??) to safely access and provide fallbacks for API fields that may be missing or null - Manage three distinct UI states: loading (show indicator), success (render data), and error (show friendly message)
- Clear the container before each render to prevent content duplication
- Handle empty arrays explicitly with a user-facing message rather than leaving a blank area