JavaScript & TypeScript Mastery

Async Patterns & Error Handling

4 min read

Asynchronous programming is where many candidates stumble. Interviewers test both your knowledge of Promise APIs and your ability to handle edge cases gracefully.

Promise Combinators

Know all four and when to use each:

const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));
const fail = new Promise((_, reject) => setTimeout(() => reject('error'), 200));

// Promise.all — resolves when ALL resolve, rejects on FIRST rejection
await Promise.all([fast, slow]);       // ['fast', 'slow']
await Promise.all([fast, fail, slow]); // throws 'error'

// Promise.allSettled — waits for ALL to settle (never rejects)
await Promise.allSettled([fast, fail, slow]);
// [
//   { status: 'fulfilled', value: 'fast' },
//   { status: 'rejected', reason: 'error' },
//   { status: 'fulfilled', value: 'slow' }
// ]

// Promise.race — resolves/rejects with the FIRST to settle
await Promise.race([fast, slow]);  // 'fast'

// Promise.any — resolves with the FIRST to fulfill (ignores rejections)
await Promise.any([fail, fast, slow]);  // 'fast'
await Promise.any([fail]);              // throws AggregateError
Combinator Resolves when Rejects when
all All fulfill First rejection
allSettled All settle Never rejects
race First settles First rejection (if first to settle)
any First fulfills All reject (AggregateError)

Implementing Promise.all

A common interview coding question:

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      resolve([]);
      return;
    }

    const results = new Array(promises.length);
    let resolvedCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = value;
          resolvedCount++;
          if (resolvedCount === promises.length) {
            resolve(results);
          }
        },
        (error) => {
          reject(error);
        }
      );
    });
  });
}

Key details interviewers check:

  • Results must maintain order (use index, not push)
  • Handle non-Promise values via Promise.resolve(promise)
  • Handle empty array edge case
  • Only call resolve once (when all are done)
  • Reject immediately on first error

AbortController

Used to cancel fetch requests and other async operations:

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

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

// AbortController can also cancel multiple operations
const controller = new AbortController();

// Pass the same signal to multiple fetch calls
const [users, posts] = await Promise.all([
  fetch('/api/users', { signal: controller.signal }),
  fetch('/api/posts', { signal: controller.signal }),
]);

// Cancel both if component unmounts (React pattern)
controller.abort();

Retry with Exponential Backoff

A practical pattern that combines async concepts:

async function retry(fn, maxRetries = 3, baseDelay = 1000) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      const delay = baseDelay * Math.pow(2, attempt);
      const jitter = delay * (0.5 + Math.random() * 0.5);
      await new Promise(resolve => setTimeout(resolve, jitter));
    }
  }
}

// Usage
const data = await retry(
  () => fetch('/api/data').then(r => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
  }),
  3,    // max 3 retries
  1000  // start with 1s delay
);

Common Async Pitfalls

Pitfall 1: forEach does not await

// WRONG — fires all requests simultaneously, does not wait
const ids = [1, 2, 3];
ids.forEach(async (id) => {
  await fetch(`/api/items/${id}`);
});
console.log('Done'); // Runs BEFORE fetches complete

// CORRECT — sequential
for (const id of ids) {
  await fetch(`/api/items/${id}`);
}
console.log('Done'); // Runs after all fetches

// CORRECT — parallel
await Promise.all(ids.map(id => fetch(`/api/items/${id}`)));
console.log('Done'); // Runs after all fetches

Pitfall 2: Unhandled Promise rejections

// WRONG — no catch, unhandled rejection
async function loadData() {
  const data = await fetch('/api/data');
  return data.json();
}
loadData(); // If this rejects, nothing catches it

// CORRECT — always handle rejections
loadData().catch(console.error);

// Or in React, use error boundaries + useEffect cleanup
useEffect(() => {
  let cancelled = false;
  loadData().then(data => {
    if (!cancelled) setData(data);
  }).catch(err => {
    if (!cancelled) setError(err);
  });
  return () => { cancelled = true; };
}, []);

Pitfall 3: Race conditions in React

// WRONG — stale data if requests complete out of order
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(setResults);
  }, [query]);
  // If query changes rapidly: "ab" request might resolve
  // after "abc" request, showing wrong results
}

// CORRECT — cancel stale requests
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });
    return () => controller.abort();
  }, [query]);
}

Interview tip: When discussing async code, always mention error handling, cancellation, and race conditions. This shows production-level thinking, not just textbook knowledge.

Now it's time to put these concepts into practice with the JavaScript Engine Simulator lab. :::

Quiz

Module 2: JavaScript & TypeScript Mastery

Take Quiz