Requests with fetch

Make a request without reloading the page, read the response correctly, send data, and stop a slow request with AbortController.

A minimal request

const res = await fetch('/api/tasks?limit=20', {
  headers: { Accept: 'application/json' },
  credentials: 'same-origin'
});

if (!res.ok) throw new Error('HTTP ' + res.status);   // fetch does not throw on 404 or 500
const data = await res.json();                        // the body can be read once
  • AJAX is not a technology: it is an ordinary HTTP request made by script whose response is used without navigating.
  • fetch resolves as soon as the response headers arrive, so the body is still pending until you read it.
  • res.ok is true for every status from 200 to 299; a 500 resolves the promise, and only a network failure rejects it.
  • Relative URLs resolve against the document, so a page at /learn/ajax/ must use /api/tasks rather than api/tasks.
⚠️
A body can only be read once. Calling res.json() after res.text() throws TypeError: body already used. Decide which reader you need, call it once, and keep the parsed value.

Sending data

await fetch('/api/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Ship it' })
});

const fd = new FormData(form);            // multipart; no header needed
await fetch('/upload', { method: 'POST', body: fd });

await fetch('/api/tasks/7', { method: 'DELETE' });
MethodIdempotent?Typical use
GETYesread a resource
POSTNocreate, submit, act
PUTYesreplace a resource
PATCHNopartial update
DELETEYesremove a resource
HEADYescheck headers only

Idempotency is what makes retries safe: repeating a PUT or DELETE leaves the same state, while repeating a POST usually creates a second record.

Timeouts, cancellation and retries

async function getWithTimeout(url, ms) {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(), ms);

  try {
    const res = await fetch(url, { signal: ac.signal });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') return null;   // cancelled on purpose
    throw err;
  } finally {
    clearTimeout(timer);
  }
}
  • fetch has no timeout option of its own; an aborted signal is the supported replacement.
  • One AbortController cancels every request that received its signal, which is exactly what a route change or a component unmount should do.
  • Retry only transient failures: network errors, 429 and 5xx. A 400 or 404 will fail identically every time.
  • Back off between attempts, cap the number of tries, and never retry a non-idempotent write without an idempotency key.

FAQ

Is fetch available everywhere?
Yes in every current browser, and in Node 18 and later. In very old code you will meet XMLHttpRequest instead, which is also what most polyfills wrap.
Why do I get a CORS error for a URL that works in the address bar?
Typing a URL sends no Origin header. A script request does, and the server has to answer with a matching Access-Control-Allow-Origin header; without it the browser withholds the response from your code.

XMLHttpRequest and the gaps in fetch Async JavaScript and fetch

Last refreshed 2026-09-18.