Fetch and Web APIs
The Fetch API is the principal modern mechanism for HTTP requests from JavaScript — a substantial replacement for XMLHttpRequest. The principal call: fetch(url, options) returns a Promise<Response>. The Response admits methods to read the body — .json(), .text(), .blob(), .arrayBuffer(), .formData(). Errors only reject for substantial network failures; HTTP error statuses (4xx, 5xx) succeed — must be checked via response.ok or response.status. The principal supporting APIs: Request, Response, Headers, URLSearchParams, FormData, AbortController (for substantial cancellation). The combination — Promise-based fetch, substantial body-format methods, substantial cancellation via AbortController, the conventional manual error-status check — is the substance of HTTP work in JavaScript.
A first request
const response = await fetch("/api/users/42");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
console.log(user.name);
The principal points:
fetchreturns a Promise —awaitadmits substantial flow.- Network errors reject — DNS failure, connection refused.
- HTTP errors do not reject — 404, 500, etc. succeed; check
response.ok.
The Response
response.ok; // true if 2xx
response.status; // 200, 404, 500, ...
response.statusText; // "OK", "Not Found"
response.headers; // Headers object
response.url; // final URL (after redirects)
response.redirected; // boolean
response.type; // "basic", "cors", "opaque"
// Body methods (consumes body — call ONE):
const json = await response.json();
const text = await response.text();
const blob = await response.blob();
const buffer = await response.arrayBuffer();
const formData = await response.formData();
// Or stream:
const reader = response.body.getReader();
The body is a stream — it can only be consumed once. Use response.clone() for substantial multiple reads.
GET request
// Simple:
const r = await fetch("/api/users");
const users = await r.json();
// With query string:
const params = new URLSearchParams({
q: "alice",
page: 1,
sort: "name"
});
const r = await fetch(`/api/search?${params}`);
// Or with URL:
const url = new URL("/api/search", window.location.origin);
url.searchParams.set("q", "alice");
const r = await fetch(url);
POST request
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Alice", email: "a@b.c" })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
POST with FormData
const form = document.querySelector("form");
const data = new FormData(form);
const response = await fetch("/api/upload", {
method: "POST",
body: data // browser sets Content-Type with boundary
});
POST with URLSearchParams
const data = new URLSearchParams();
data.append("username", "alice");
data.append("password", "secret");
const response = await fetch("/api/login", {
method: "POST",
body: data // application/x-www-form-urlencoded
});
Other methods
// PUT:
await fetch("/api/users/42", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user)
});
// PATCH:
await fetch("/api/users/42", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Updated" })
});
// DELETE:
await fetch("/api/users/42", { method: "DELETE" });
Headers
const headers = new Headers({
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
});
headers.set("X-Custom", "value");
headers.append("X-Multi", "first");
headers.append("X-Multi", "second");
headers.has("Authorization");
headers.get("Content-Type");
headers.delete("X-Custom");
for (const [key, value] of headers) {
console.log(key, value);
}
await fetch(url, { headers });
// Or as plain object:
await fetch(url, {
headers: { "Authorization": `Bearer ${token}` }
});
Reading response body
const r = await fetch("/api/data");
// JSON:
const data = await r.json();
// Text:
const text = await r.text();
// Binary (Blob):
const blob = await r.blob();
const url = URL.createObjectURL(blob);
img.src = url;
// Binary (ArrayBuffer):
const buffer = await r.arrayBuffer();
const view = new Uint8Array(buffer);
// FormData (rarely needed):
const formData = await r.formData();
Streaming response
const r = await fetch("/large-file");
const reader = r.body.getReader();
let bytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.length;
console.log(`received ${bytes} bytes`);
}
For substantial line-by-line:
const r = await fetch("/api/stream");
const reader = r.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
let idx;
while ((idx = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
handleLine(line);
}
}
Error handling
try {
const r = await fetch("/api/data");
if (!r.ok) {
// HTTP error (4xx, 5xx):
throw new Error(`HTTP ${r.status}: ${r.statusText}`);
}
const data = await r.json();
return data;
} catch (err) {
if (err instanceof TypeError) {
// Network error
console.error("Network error:", err);
} else {
// HTTP error
console.error("API error:", err);
}
throw err;
}
A substantial reusable pattern:
class HttpError extends Error {
constructor(message, status, body) {
super(message);
this.name = "HttpError";
this.status = status;
this.body = body;
}
}
async function api(url, options = {}) {
const r = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...(options.headers ?? {})
},
body: options.body && typeof options.body !== "string"
? JSON.stringify(options.body)
: options.body
});
if (!r.ok) {
let body;
try { body = await r.json(); } catch { body = await r.text(); }
throw new HttpError(`HTTP ${r.status}`, r.status, body);
}
if (r.status === 204) return null;
const ct = r.headers.get("Content-Type") ?? "";
if (ct.includes("application/json")) return await r.json();
return await r.text();
}
const user = await api("/api/users/42");
const newUser = await api("/api/users", { method: "POST", body: { name: "A" } });
Cancellation
The AbortController admits substantial cancellation:
const controller = new AbortController();
const response = await fetch("/api/data", { signal: controller.signal });
// Later:
controller.abort();
The fetch rejects with AbortError:
try {
const r = await fetch(url, { signal });
return await r.json();
} catch (err) {
if (err.name === "AbortError") {
console.log("request was cancelled");
return null;
}
throw err;
}
Timeout
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const r = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
return await r.json();
} catch (err) {
if (err.name === "AbortError") {
throw new Error("Request timed out");
}
throw err;
}
Or with AbortSignal.timeout (modern):
const r = await fetch(url, { signal: AbortSignal.timeout(5000) });
Combined signals
const userController = new AbortController();
const r = await fetch(url, {
signal: AbortSignal.any([userController.signal, AbortSignal.timeout(5000)])
});
CORS
The browser enforces Cross-Origin Resource Sharing (CORS):
- Same origin — full access.
- Cross-origin GET/POST (simple) — browser sends; server must respond with
Access-Control-Allow-Origin. - Pre-flight (PUT, DELETE, custom headers, JSON content-type) — browser sends
OPTIONSfirst.
// CORS request:
const r = await fetch("https://api.other-site.com/data", {
mode: "cors", // default for cross-origin
credentials: "include" // send cookies
});
// Modes:
// "cors" — default; respects CORS
// "no-cors" — opaque response; can't read body
// "same-origin" — fail on cross-origin
// "navigate" — for navigation
For cookies on cross-origin:
// Send cookies:
fetch(url, { credentials: "include" });
// Don't send (default for cross-origin):
fetch(url, { credentials: "omit" });
// Same-origin only (default for same-origin):
fetch(url, { credentials: "same-origin" });
File uploads
const file = fileInput.files[0];
// Simple:
const data = new FormData();
data.append("file", file);
await fetch("/api/upload", {
method: "POST",
body: data
});
// With progress (no native — use XHR or stream):
const r = await fetch("/api/upload", {
method: "POST",
body: file, // Blob/File OK directly
headers: { "Content-Type": file.type }
});
For progress, the contemporary approach uses ReadableStream:
const file = fileInput.files[0];
const total = file.size;
let uploaded = 0;
const stream = file.stream().pipeThrough(new TransformStream({
transform(chunk, controller) {
uploaded += chunk.length;
progressBar.value = (uploaded / total) * 100;
controller.enqueue(chunk);
}
}));
await fetch("/api/upload", {
method: "POST",
body: stream,
duplex: "half",
headers: { "Content-Type": file.type }
});
(Streaming request bodies require modern browsers.)
Download with progress
const r = await fetch(url);
const total = parseInt(r.headers.get("Content-Length") ?? "0");
const reader = r.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
progressBar.value = (received / total) * 100;
}
const blob = new Blob(chunks);
const url = URL.createObjectURL(blob);
Common patterns
Retry with exponential backoff
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i <= retries; i++) {
try {
const r = await fetch(url, options);
if (r.ok) return r;
if (r.status >= 500 && i < retries) {
await new Promise(res => setTimeout(res, 2 ** i * 1000));
continue;
}
throw new HttpError(`HTTP ${r.status}`, r.status);
} catch (err) {
if (err.name === "AbortError" || i === retries) throw err;
await new Promise(res => setTimeout(res, 2 ** i * 1000));
}
}
}
Parallel requests
const [users, posts] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]);
All-settled
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
fetch("/api/c")
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("ok", result.value);
} else {
console.log("err", result.reason);
}
}
Cancel previous request on new
let controller;
async function search(query) {
controller?.abort();
controller = new AbortController();
try {
const r = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
return await r.json();
} catch (err) {
if (err.name === "AbortError") return null;
throw err;
}
}
Authenticated requests
async function api(url, options = {}) {
return fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${getToken()}`,
"Content-Type": "application/json",
...(options.headers ?? {})
}
});
}
Refresh token on 401
async function api(url, options = {}) {
let r = await authedFetch(url, options);
if (r.status === 401) {
await refreshToken();
r = await authedFetch(url, options);
}
if (!r.ok) throw new HttpError(`HTTP ${r.status}`, r.status);
return r.json();
}
JSON helper
async function getJson(url, options = {}) {
const r = await fetch(url, options);
if (!r.ok) throw new HttpError(`HTTP ${r.status}`, r.status);
return r.json();
}
async function postJson(url, body, options = {}) {
return getJson(url, {
...options,
method: "POST",
headers: {
"Content-Type": "application/json",
...(options.headers ?? {})
},
body: JSON.stringify(body)
});
}
Request with query params
async function get(path, params = {}) {
const url = new URL(path, window.location.origin);
for (const [k, v] of Object.entries(params)) {
if (v != null) url.searchParams.set(k, v);
}
const r = await fetch(url);
if (!r.ok) throw new HttpError(`HTTP ${r.status}`, r.status);
return r.json();
}
const users = await get("/api/users", { q: "alice", page: 1 });
Cache-busting
const url = new URL("/api/data", window.location.origin);
url.searchParams.set("_", Date.now());
const r = await fetch(url, { cache: "no-store" });
Cache control
fetch(url, {
cache: "default" // browser default
});
fetch(url, { cache: "no-store" }); // never cache
fetch(url, { cache: "no-cache" }); // revalidate
fetch(url, { cache: "force-cache" }); // use cache if any
fetch(url, { cache: "only-if-cached" }); // cache or fail
fetch(url, { cache: "reload" }); // bypass cache for fetch
Server-Sent Events
const events = new EventSource("/api/stream");
events.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
console.log(data);
});
events.addEventListener("custom-event", (e) => {
console.log(e.data);
});
events.addEventListener("error", () => {
console.error("connection error");
});
// Close:
events.close();
WebSocket
const ws = new WebSocket("wss://example.com/ws");
ws.addEventListener("open", () => {
ws.send("hello");
});
ws.addEventListener("message", (e) => {
console.log(e.data);
});
ws.addEventListener("close", () => {
console.log("closed");
});
ws.addEventListener("error", (err) => {
console.error(err);
});
// Close:
ws.close();
Request and Response reuse
const req = new Request("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user)
});
const r1 = await fetch(req.clone());
const r2 = await fetch(req); // can reuse once
A note on XMLHttpRequest
The legacy XMLHttpRequest (XHR) is still present:
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.responseType = "json";
xhr.onload = () => console.log(xhr.response);
xhr.send();
The conventional contemporary discipline uses fetch — substantial cleaner with Promises and await. XHR remains the substantial mechanism for upload progress (until streaming bodies are universally supported).
A note on the conventional discipline
The contemporary fetch advice:
- Use
fetchoverXMLHttpRequest. - Always check
response.ok— fetch does not reject on HTTP errors. - Use
AbortControllerfor substantial cancellation and timeouts. - Use
AbortSignal.timeout(ms)for substantial simple timeouts. - Use
URLSearchParamsfor substantial query strings. - Use
URLover manual URL construction. - Use
FormDatafor file uploads. - Wrap fetch in a typed/error-handling wrapper.
- Use
Promise.allfor parallel requests. - Use
Promise.allSettledwhen all results matter. - Cancel stale requests (search-as-you-type pattern).
- Set
credentials: "include"for cookie-bearing cross-origin. - Use
EventSourcefor substantial server-push (one-way). - Use
WebSocketfor substantial bidirectional real-time. - Reach for a library (axios, ky, ofetch) for substantial enterprise patterns — interceptors, retries, etc.
The combination — Promise-based fetch, the substantial Response body methods, the conventional manual error-status check, the substantial cancellation via AbortController, the conventional supporting APIs (URLSearchParams, FormData, Headers), the substantial CORS model — is the substance of HTTP work in JavaScript. The discipline produces clean, cancellable, error-aware HTTP code.