Home JS

Fetch API: requests, responses, streaming, and abort

Fetch is the modern HTTP client built into the platform. It exposes Request, Response, AbortSignal, ReadableStream, and FormData as primitives that compose with the rest of the platform.

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.
  • AbortSignal and AbortController — interop 2019.
  • Response.body streams — interop 2020.
  • AbortSignal.timeout() — interop 2023.
  • Streamed request bodies — Chromium (Blink) and Gecko 2023; WebKit pending as of 2025.
  • Service worker respondWith integration — interop since 2017 (see /js/service-worker).

Common pitfalls

  • Treating fetch as throw-on-non-2xx. It does not. Check res.ok.
  • Setting Content-Type: application/json on a FormData body. 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