Modern Fetch API Patterns
Timeouts, retries, cancellation, and progress with the built-in fetch.
Timeouts via AbortSignal
``js
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
`
AbortSignal.timeout is built in across modern runtimes. No setTimeout/clearTimeout dance.
Combining Signals
`js
const userAbort = new AbortController();
const signal = AbortSignal.any([userAbort.signal, AbortSignal.timeout(10000)]);
fetch(url, { signal });
`
AbortSignal.any (Node 20+, Chrome 116+) lets either source cancel the request.
Retry with Exponential Backoff
`js
async function fetchRetry(url, opts, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url, opts);
if (res.ok) return res;
if (res.status < 500) return res; // do not retry 4xx
} catch (e) {
if (i === retries - 1) throw e;
}
await new Promise(r => setTimeout(r, 2 ** i * 200));
}
}
`
Streaming Response
`js
const res = await fetch(url);
for await (const chunk of res.body) {
// chunk is Uint8Array
}
`
Upload Progress (with Streams)
Native fetch finally supports request streams in Chromium and Safari 18:
`js
fetch('/upload', {
method: 'POST',
body: fileStream,
duplex: 'half'
});
`
JSON Defaults Wrapper
`js
async function api(url, body) {
const res = await fetch(url, {
method: body ? 'POST' : 'GET',
headers: { 'content-type': 'application/json' },
body: body && JSON.stringify(body),
signal: AbortSignal.timeout(8000)
});
if (!res.ok) throw new Error(${res.status} ${res.statusText});
return res.json();
}
``
That 12-line wrapper covers 90% of frontend HTTP needs.
For service worker integration see [service workers PWA guide](/blog/service-workers-pwa-guide).