Codeskill

Learn to code, step by step

Async patterns (errors, cancellation)

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.

PreviousClasses and prototypes