fetch is the browser's built-in way to call APIs — every weather widget, login form and search box uses it. Its one dangerous quirk: a 404 does not throw an error unless you check for it.
GET: const data = await (await fetch(url)).json() — but always check res.ok first, because fetch only rejects on network failure, not on 404/500. POST JSON: pass { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(obj) }.
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) { // THE line everyone forgets
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return await res.json(); // parse JSON body
}
const user = await getUser(1);
console.log(user.name);const res = await fetch("/api/scores", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ player: "Ada", score: 950 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const saved = await res.json();
// other verbs work the same: method: "PUT" | "PATCH" | "DELETE"async function loadSafely(url) {
try {
const res = await fetch(url, {
signal: AbortSignal.timeout(8000), // give up after 8s
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === "TimeoutError") return { error: "Too slow" };
if (err.name === "TypeError") return { error: "Offline?" };
return { error: err.message }; // HTTP errors from above
}
}async function api(url, options = {}) {
const res = await fetch(url, {
headers: { "Content-Type": "application/json", ...options.headers },
...options,
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.status === 204 ? null : res.json();
}
// usage:
const users = await api("/api/users");
await api("/api/users", { method: "POST", body: { name: "Ada" } });By design, fetch only rejects on network-level failures (offline, DNS, CORS). HTTP error statuses are 'successful' responses you must check via res.ok.
Browsers block cross-origin requests unless the server sends Access-Control-Allow-Origin headers permitting your site. It's a server-side setting — you can't fix it purely from the client.
Pass a FormData object as body (no Content-Type header needed): fetch(url, { method: "POST", body: new FormData(form) }).
Use AbortSignal.timeout(ms) as the signal option, or an AbortController for manual cancellation.