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.
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
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.
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.