Glossary

Fetch API

Fetch API is the promise-based interface that browsers and Node.js use to send HTTP requests and read the responses, replacing XMLHttpRequest for most code. It is defined by the WHATWG Fetch Standard, a living standard. The question people search most is why fetch does not throw on a 404, and the answer is that only network failures reject.

How it works

You call fetch(input, init), where input is a URL string or a Request object and init is an optional dictionary. It returns a Promise that resolves to a Response as soon as the status line and headers arrive. The body has not been read yet. You read it once, with one of the body methods: json(), text(), blob(), arrayBuffer(), bytes() or formData().

The init dictionary controls the request. Common members are method (default GET), headers, body, signal, credentials, redirect and keepalive. The credentials values are "omit", "same-origin" and "include". The redirect values are "follow", "error" and "manual".

A body can be a string, Blob, ArrayBuffer, URLSearchParams, FormData or a stream. When you pass a FormData object, fetch builds a multipart/form-data body and sets the Content-Type header, boundary included, for you.

The Response exposes status, statusText, headers, url, redirected, type and ok. In the standard, an ok status is any status from 200 to 299 inclusive.

const http = require('http');
const srv = http.createServer((req, res) => {
  if (req.url === '/missing') { res.statusCode = 404; res.end('nope'); }
  else { res.setHeader('content-type', 'application/json'); res.end('{"id":7}'); }
}).listen(0, async () => {
  const base = 'http://127.0.0.1:' + srv.address().port;
  const r404 = await fetch(base + '/missing');
  console.log('404 resolved:', r404.ok, r404.status);
  const r = await fetch(base + '/user');
  console.log(r.ok, r.status, r.headers.get('content-type'));
  console.log(await r.json());
  try { await r.json(); } catch (e) { console.log(e.name + ': ' + e.message, r.bodyUsed); }
  try { await fetch('http://127.0.0.1:1/'); } catch (e) { console.log(e.name + ': ' + e.message); }
  srv.close();
});

Output from Node 22.22.0:

404 resolved: false 404
true 200 application/json
{ id: 7 }
TypeError: Body is unusable: Body has already been read true
TypeError: fetch failed

Why does fetch not throw on 404 or 500?

Fetch resolves for any HTTP response the server sends, because a 404 is a successful HTTP exchange from the network's point of view. The promise rejects with a TypeError only when the request cannot complete, for example a DNS failure, a refused connection, a CORS block or a blocked mixed-content request. Check response.ok and throw yourself if you want status errors to land in catch.

How do I add a timeout to fetch?

Pass a signal that aborts after a delay: fetch(url, { signal: AbortSignal.timeout(5000) }). Fetch has no timeout option of its own and, by default, waits until the browser gives up. When the timer fires, the promise rejects with a TimeoutError DOMException. See AbortController for the details.

Common pitfalls

  • Treating a resolved promise as success: a 500 page resolves normally. Test response.ok or response.status before parsing the body.
  • Reading the body twice: a second json() call rejects with a TypeError ("Body is unusable") and bodyUsed is already true. Call clone() before reading if you need two reads.
  • Setting Content-Type by hand for FormData: your header lacks the boundary, so the server cannot split the parts. Leave the header unset and let fetch fill it.
  • Forgetting credentials: cross-origin requests send no cookies unless you set credentials to "include", and the server must also allow it under CORS.
  • Sending large keepalive bodies: with keepalive true, request bodies are capped by a 64 kibibyte quota in the standard, and the call fails with a network error beyond it.
  • Parsing JSON from an error page: a 502 from a proxy is often HTML, so json() throws a SyntaxError. Check the Content-Type header first.

Related terms

  • Promise — fetch() returns one, and it settles with a Response or a TypeError.
  • Async/await — the usual way to write sequential fetch calls without nested callbacks.
  • CORS — the browser policy that decides whether a cross-origin response is readable.
  • HTTP — the protocol whose methods, headers and status codes fetch exposes.
  • AbortController — cancels an in-flight fetch or enforces a timeout.
  • FormData — builds multipart request bodies for uploads.

See also