Back to Blog
Web APIs2026-04-23

Modern Fetch API Patterns

Timeouts, retries, cancellation, and progress with the built-in fetch.

The native fetch API has caught up to dedicated HTTP libraries. With AbortController, AbortSignal.timeout, and streaming bodies, you rarely need axios anymore.

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