fetch in the Real World
intermediate16 min readLesson 75 of 143
Status handling, JSON bodies, headers, query params, pagination loops, and the loading/error UI contract.
fetch resolves on any response โ 404 and 500 included. Only network
failure rejects. The first professional habit: check response.ok.
The shape every call should have
async function getUser(id) {
const res = await fetch("/api/users/" + id);
if (!res.ok) {
throw new Error("HTTP " + res.status + " for user " + id);
}
return res.json(); // also a promise
}
res.okisstatusin 200โ299.res.json()parses the body;res.text()for anything else.- Request options:
method,headers,body(JSON needsJSON.stringifyplusContent-Type: application/json).
fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "write tests" }),
});
Query params and pagination
Build queries with URLSearchParams โ it encodes for free:
const qs = new URLSearchParams({ page: String(page), perPage: "20", q: term });
const res = await fetch("/api/items?" + qs);
Paginated loops continue while a has-more signal exists (a next link, or
page * perPage < total) โ never blindly until an empty page without a bound.
The UI contract
Every async view owes the user three states: loading (spinner/skeleton),
error (message + retry affordance), data (or a polite empty state).
Design the component around { status: "idle" | "loading" | "error" | "success", data, error } and rendering becomes trivial.