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.
fetchresolves as soon as the response headers arrive, so the body is still pending until you read it.res.okis 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/tasksrather thanapi/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' });| Method | Idempotent? | Typical use |
|---|---|---|
| GET | Yes | read a resource |
| POST | No | create, submit, act |
| PUT | Yes | replace a resource |
| PATCH | No | partial update |
| DELETE | Yes | remove a resource |
| HEAD | Yes | check 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);
}
}fetchhas no timeout option of its own; an aborted signal is the supported replacement.- One
AbortControllercancels 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.Related
XMLHttpRequest and the gaps in fetch Async JavaScript and fetch
Last refreshed 2026-09-18.