Polyglot
Languages Web (HTML / CSS / JS) fetch
Web (HTML / CSS / JS) § fetch

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:

  • fetch returns a Promiseawait admits 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 OPTIONS first.
// 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 fetch over XMLHttpRequest.
  • Always check response.ok — fetch does not reject on HTTP errors.
  • Use AbortController for substantial cancellation and timeouts.
  • Use AbortSignal.timeout(ms) for substantial simple timeouts.
  • Use URLSearchParams for substantial query strings.
  • Use URL over manual URL construction.
  • Use FormData for file uploads.
  • Wrap fetch in a typed/error-handling wrapper.
  • Use Promise.all for parallel requests.
  • Use Promise.allSettled when all results matter.
  • Cancel stale requests (search-as-you-type pattern).
  • Set credentials: "include" for cookie-bearing cross-origin.
  • Use EventSource for substantial server-push (one-way).
  • Use WebSocket for 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.