Async patterns beyond a single await fetch – error handling, timeouts, cancellation, and concurrency limits. The intro series showed promises; here we use them like production code.
try/catch with async/await
async function loadProfile(userId) {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (err) {
console.error('Profile load failed', err);
throw err;
}
}
Check res.ok. Fetch only rejects on network failure, not 404 or 500.
Parallel vs sequential
// Sequential - second waits for first
const a = await fetch('/a');
const b = await fetch('/b');
// Parallel - both in flight
const [aRes, bRes] = await Promise.all([
fetch('/a'),
fetch('/b'),
]);
Use Promise.all when tasks are independent. If one failure should not kill the rest, Promise.allSettled returns status per promise.
const results = await Promise.allSettled([
fetch('/a'),
fetch('/b'),
]);
results.forEach((r) => {
if (r.status === 'fulfilled') {
console.log('OK', r.value.status);
} else {
console.warn('Failed', r.reason);
}
});
Timeouts
Fetch has no built-in timeout. Wrap it with AbortSignal.timeout (modern browsers) or race against a timer.
async function fetchWithTimeout(url, ms = 8000) {
const res = await fetch(url, { signal: AbortSignal.timeout(ms) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
Cancellation with AbortController
let controller = new AbortController();
searchInput.addEventListener('input', debounce(async () => {
controller.abort();
controller = new AbortController();
const { signal } = controller;
try {
const res = await fetch(`/search?q=${encodeURIComponent(q)}`, { signal });
renderResults(await res.json());
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}, 300));
Abort stale requests when the user types quickly. Ignore AbortError – it means you cancelled on purpose.
Concurrency limits
Firing five hundred fetch calls at once can overwhelm the browser or API. Process in batches:
async function mapPool(items, limit, fn) {
const results = [];
let index = 0;
async function worker() {
while (index < items.length) {
const i = index++;
results[i] = await fn(items[i]);
}
}
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
Retry with backoff
Retry idempotent reads on transient failures – not POST payments without an idempotency key. Exponential backoff reduces hammering a struggling server.
async function fetchRetry(url, attempts = 3) {
let delay = 300;
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url);
if (res.ok) return res;
} catch (_) {
if (i === attempts - 1) throw _;
}
await new Promise((r) => setTimeout(r, delay));
delay *= 2;
}
throw new Error('Failed after retries');
}
Next: the Fetch API in depth – headers, bodies, CORS at a practical level, and uploads.

