All topics
Asyncintermediate

The Fetch API

The modern, Promise-based browser API for making HTTP requests, replacing XMLHttpRequest for most use cases.

fetch makes HTTP requests and returns a promise. It replaced XMLHttpRequest for almost everything.

fetch is like ordering delivery: the promise resolving is the doorbell ringing — it doesn't mean the food is good or that you've opened the box yet.

Key Concepts

1
    const res = await fetch('/api/users');
    const data = await res.json();
2
Two awaits, because the body arrives separately. The first promise resolves when the headers are in; the body is still streaming. .json(), .text() and .blob() each return a promise for the parsed body.
.json().text().blob()
3
The trap that catches everyone: a 404 does not reject.
    const res = await fetch('/missing');
    // no error -- res.ok is false, res.status is 404
4
fetch rejects only on a network failure — DNS, no connection, CORS. Any HTTP response, including 500, is a successful fetch.
fetch
5
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
HTTP ${res.status}
6
That check is required on every call, and omitting it means parsing an error page as JSON and failing with a confusing message.
7
A POST needs three things.
    await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user)
    });
8
body must be a string, FormData, Blob or similar — not an object. Passing an object sends [object Object], and the header must match what you actually sent.
bodyFormDataBlob[object Object]
9
With FormData, do not set Content-Type — the browser sets it including the multipart boundary, and setting it by hand breaks the request.
FormDataContent-Type
10
Cancellation is AbortController.
    const c = new AbortController();
    fetch(url, { signal: c.signal });
    c.abort();      // the promise rejects with an AbortError
AbortController
11
That is also how a timeout is implemented properly, since Promise.race leaves the request running.
Promise.race
12
Credentials are not sent by default cross-origin.
    credentials: 'same-origin'   // the default
    credentials: 'include'       // send cookies cross-origin
13
The body can be read once.
    await res.json();
    await res.text();     // TypeError: body already read
14
res.clone() if you need it twice — logging the raw text and then parsing, for example.
res.clone()
15
What the interviewer is probing.1. "Does fetch reject on a 404?" Probing: the classic gotcha. Stalls: "Yes." Moves up: no — it resolves, and only rejects on a network failure; you must check response.ok yourself.
16
2. "How do you cancel a fetch?" Probing: AbortController. Stalls: "You cannot." Moves up: pass an AbortController signal and call abort, which is also how you implement a real timeout.
17
3. "Why can you only read the body once?" Probing: the stream. Stalls: "You can read it repeatedly." Moves up: the body is a stream consumed on read; clone the response if you need it twice.
18
4. "What does credentials: include do?" Probing: cookies and CORS. Stalls: "Adds a header." Moves up: it sends cookies cross-origin, which also requires the server to allow credentials and name a specific origin rather than a wildcard.