What Fetch is
fetch() is the platform HTTP client. It returns a Promise<Response>
that resolves once response headers arrive — not once the body has
been consumed. Reading the body is a separate await:
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const users = await res.json();
res.ok is true only for 2xx status. The Promise rejection only
fires on network errors and CORS failures — 404 and 500 resolve
normally. This catches many bugs.
Request and Response are first-class objects
Fetch is built around two constructors. Request represents the
request the platform will send; Response represents the reply.
Both can be constructed manually, useful for service workers and
testing:
const req = new Request('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const res = new Response('ok', { status: 200 });
Request and Response are consumable: their bodies are streams.
Reading req.json() consumes the body; a second read throws. Use
req.clone() if you need to read twice.
Abort with AbortSignal
Every long-running platform operation now accepts an AbortSignal.
Fetch is the canonical example:
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(new Error('timeout')), 5000);
try {
const res = await fetch(url, { signal: ac.signal });
// ...
} finally {
clearTimeout(timer);
}
AbortSignal.timeout(ms) is shorthand for the pattern above. Multiple
signals can be combined with AbortSignal.any([s1, s2]). Aborting after
the response headers arrive but before the body is consumed cancels
the in-flight body stream.
Streaming bodies
Response.body is a ReadableStream of Uint8Array chunks. For
large responses, the stream lets you process bytes as they arrive
rather than buffering everything. The web TransformStream gives a
pipe API to transcode line-delimited JSON:
const res = await fetch('/api/events');
const lines = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
for (const line of chunk.split('\n')) {
if (line.trim()) controller.enqueue(JSON.parse(line));
}
},
}));
for await (const event of lines) {
// ...
}
Streamed uploads require body: stream plus duplex: 'half' and
are supported in Chromium (Blink) and Gecko but not yet in WebKit
as of 2025.
Sending FormData
Pass a FormData instance directly as the body — the user agent
sets the multipart Content-Type and boundary for you:
const fd = new FormData(form);
await fetch('/upload', { method: 'POST', body: fd });
Mixing Content-Type: multipart/form-data headers with FormData
breaks the boundary. Let the user agent set the header.
Cookies and credentials
Fetch defaults to credentials: 'same-origin'. Cross-origin requests
that need cookies must explicitly set credentials: 'include', and
the server must reply with Access-Control-Allow-Credentials: true
plus a non-wildcard Access-Control-Allow-Origin. Forgetting either
is a frequent cause of “the browser swallowed my cookie” bugs.
Browser engine support
- Core
fetch()— interop since 2017 across Chromium (Blink), WebKit, and Gecko. AbortSignalandAbortController— interop 2019.Response.bodystreams — interop 2020.AbortSignal.timeout()— interop 2023.- Streamed request bodies — Chromium (Blink) and Gecko 2023; WebKit pending as of 2025.
- Service worker
respondWithintegration — interop since 2017 (see /js/service-worker).
Common pitfalls
- Treating
fetchas throw-on-non-2xx. It does not. Checkres.ok. - Setting
Content-Type: application/jsonon aFormDatabody. Breaks multipart boundary detection. - Reading the body twice. Use
clone()or capture the parsed result in a variable. mode: 'no-cors'to “make CORS go away”. It returns an opaque response whose body is unreadable.- Long-lived requests without
AbortSignal. Memory accumulates; pages close while a fetch is still pending.
Further reading
- WHATWG Fetch Living Standard.
- The Streams Living Standard (streams.spec.whatwg.org) defines
ReadableStream,WritableStream, andTransformStream— Fetch leans on all three. - Anne van Kesteren’s notes on the half-duplex constraint and the 2023 introduction of
duplex: 'half'for streamed uploads. - Jake Archibald’s article on the request body streams interop gap (2023) is the clearest summary of why WebKit support remains pending.
- About 98% of the Fetch test suite passes in Chromium, WebKit, and Gecko in the Interop 2024 dashboard.