Async JavaScript trips up even experienced developers. Here's a reference for the patterns that actually matter in day-to-day work.

The Basics: async/await

async/await is syntactic sugar over Promises. Under the hood, it's the same — but it reads like synchronous code.

async function fetchUser(id) {
  const res  = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return data;
}

Always handle errors. Unhandled promise rejections will crash Node processes and silently fail in browsers.

async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('Failed to fetch user:', err);
    throw err; // re-throw so the caller knows
  }
}

Running Things in Parallel

The most common async mistake: needlessly sequential awaits.

// ❌ Sequential — takes 600ms total
const user    = await fetchUser(id);     // 200ms
const posts   = await fetchPosts(id);   // 200ms
const friends = await fetchFriends(id); // 200ms

// ✅ Parallel — takes 200ms total
const [user, posts, friends] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchFriends(id),
]);

Use Promise.all whenever the requests are independent of each other.

Promise Combinators

The four combinators — each for a different use case:

Method Resolves when Rejects when
Promise.all All resolve Any rejects
Promise.allSettled All settle (resolve or reject) Never
Promise.race First settles First settles (with rejection)
Promise.any First resolves All reject
// Use allSettled when you want all results, even partial failures
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(999), // might 404
]);

for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.warn('Failed:', result.reason);
  }
}
// Use any for "first success wins" — e.g. redundant endpoints
const data = await Promise.any([
  fetch('https://primary.api.com/data'),
  fetch('https://fallback.api.com/data'),
]);

Async Iteration

When dealing with paginated APIs or streams, for await...of keeps code clean:

async function* paginate(url) {
  let nextUrl = url;
  while (nextUrl) {
    const res  = await fetch(nextUrl);
    const page = await res.json();
    yield page.items;
    nextUrl = page.nextPage ?? null;
  }
}

for await (const items of paginate('/api/posts?limit=20')) {
  console.log('got batch:', items.length);
}

Timeout Pattern

fetch doesn't time out by default. Wrap it with AbortController:

async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  try {
    const res = await fetch(url, { signal: controller.signal });
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

Retry with Exponential Backoff

async function withRetry(fn, { retries = 3, baseDelay = 300 } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === retries) throw err;
      const delay = baseDelay * 2 ** attempt + Math.random() * 100;
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

// Usage
const data = await withRetry(() => fetchWithTimeout('/api/data'));

Common Pitfalls

Forgetting await in loops

// ❌ This fires all requests but doesn't wait for them
for (const id of ids) {
  await processItem(id); // sequential by accident — use Promise.all instead
}

// ✅ Parallel
await Promise.all(ids.map(id => processItem(id)));

async in array callbacks

// ❌ .forEach doesn't await — the loop completes before promises resolve
items.forEach(async (item) => {
  await save(item); // ignored!
});

// ✅ Use a for...of loop or Promise.all + .map
await Promise.all(items.map(item => save(item)));

These patterns cover the majority of real-world async code. The key is knowing which combinator fits the situation and never accidentally going sequential when parallel is possible.