Notifications

No notifications

fetch is the modern web API for HTTP — built into browsers and Node 18+. Returns a Promise, supports streams, headers, AbortController, and FormData. This page covers the everyday recipes (GET/POST/PUT/DELETE), error patterns (fetch only rejects on NETWORK failure — not on 404/500!), and how to add timeouts and cancellation.

On this page

Detailed Theory

# Fetch & HTTP Requests

A simple GET

const res  = await fetch("/api/users");
const data = await res.json();

The Response object

propertymeaning
res.oktrue if status is 200–299
res.statusnumeric status (200, 404, 500…)
res.statusText"OK", "Not Found" …
res.headersHeaders object — headers.get("content-type")

Body methods (each can be called only once — body is a stream):

await res.json();
await res.text();
await res.blob();
await res.formData();
await res.arrayBuffer();

⚠️ The big gotcha — fetch ONLY rejects on network failure

A 404 or 500 is a *successful* HTTP exchange — fetch resolves. You MUST check res.ok yourself:
const res = await fetch("/api/users/999");
if (!res.ok) {
    throw new Error(HTTP ${res.status} — ${res.statusText});
}
const user = await res.json();

POST with JSON body

const res = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name: "Alice", age: 25 }),
});

PUT, PATCH, DELETE

Same shape — change method. DELETE usually has no body.

FormData (file upload)

const fd = new FormData();
fd.append("file", fileInput.files[0]);
fd.append("title", "Avatar");

await fetch("/api/upload", { method: "POST", body: fd }); // ⚠️ DO NOT set Content-Type — the browser adds the multipart boundary

Query string / URLSearchParams

const params = new URLSearchParams({ q: "hello", page: 2 });
await fetch(/api/search?${params});

Headers

await fetch("/api/secure", {
    headers: {
        "Authorization": Bearer ${token},
        "Accept": "application/json",
    },
});

Cookies / credentials

fetch("/api", { credentials: "include" });   // send cookies cross-origin
include requires the server to send proper CORS headers (Access-Control-Allow-Credentials: true and a specific Allow-Origin).

Timeout + cancellation

async function fetchWithTimeout(url, ms = 5000, opts = {}) {
    const ac = new AbortController();
    const id = setTimeout(() => ac.abort(), ms);
    try {
        return await fetch(url, { ...opts, signal: ac.signal });
    } finally {
        clearTimeout(id);
    }
}

// modern shortcut (Node 17+/browsers) fetch(url, { signal: AbortSignal.timeout(5000) });

Reusable wrapper

async function api(path, { method = "GET", body, ...rest } = {}) {
    const res = await fetch(/api${path}, {
        method,
        headers: { "Content-Type": "application/json", ...(rest.headers || {}) },
        body: body && JSON.stringify(body),
        ...rest,
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(API ${res.status}: ${text});
    }
    return res.status === 204 ? null : res.json();
}

// usage const user = await api("/users/1"); await api("/users", { method: "POST", body: { name: "Alice" } });

CORS in one paragraph

Browsers block cross-origin requests unless the server replies with the right Access-Control-Allow-* headers. CORS is a *browser* enforcement — server-to-server fetches don't have it. mode: "no-cors" is almost never what you want; it returns an opaque response you can't read.

Streaming responses (advanced)

const res    = await fetch("/api/big");
const reader = res.body.getReader();
while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    // value is a Uint8Array chunk
}