Promises Deep Dive

Med

Promises are JavaScript's primary abstraction for asynchronous operations. Understanding Promise internals, combinators (all/race/any/allSettled), and error handling patterns is essential for writing robust async code.

Interactive Visualization

CodeCreation
1const p = new Promise((resolve, reject) => {
2 setTimeout(() => {
3 resolve("Success!");
4 }, 1000);
5});
6
7p.then(value => {
8 console.log(value);
9});
Promise States
p
PENDING
Console Output
Promise created - starts in PENDING state
1 / 4
Key Insight: A Promise transitions from pending → fulfilled (or rejected) exactly once. Once settled, it never changes.

Key Points

  • A Promise represents a value that may be available now, later, or never
  • Promise states: pending → fulfilled OR rejected (immutable once settled)
  • Promise.all() fails fast: rejects on first rejection
  • Promise.race() returns first settled (fulfilled OR rejected)
  • Promise.any() returns first fulfilled (ignores rejections until all fail)
  • Promise.allSettled() waits for all, never short-circuits
  • .then() always returns a new Promise (enabling chaining)
  • Unhandled rejections are dangerous - always add .catch() or try/catch

Code Examples

Promise States

// Creating Promises
const pending = new Promise(() => {});  // stays pending
const fulfilled = Promise.resolve(42);  // immediately fulfilled
const rejected = Promise.reject('err'); // immediately rejected

// State transitions are ONE-WAY and IMMUTABLE
const p = new Promise((resolve, reject) => {
  resolve('first');   // Promise is now fulfilled
  resolve('second');  // ignored!
  reject('error');    // ignored!
});

p.then(v => console.log(v));  // "first"

Once a Promise settles, its state and value are locked forever

Promise.all() - Fail Fast

// All must succeed
const results = await Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
]);
// results = [usersResponse, postsResponse, commentsResponse]

// One failure = immediate rejection
await Promise.all([
  Promise.resolve(1),
  Promise.reject('Error!'),  // Fails here
  Promise.resolve(3)          // Never awaited!
]);
// Throws: "Error!"

Use all() when you need ALL results and want fast failure

Promise.race() - First Wins

// Timeout pattern
async function fetchWithTimeout(url, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([fetch(url), timeout]);
}

// First to settle wins (success OR failure)
await Promise.race([
  fetch('/slow'),    // takes 5s
  fetch('/fast')     // takes 1s - this wins!
]);

Use race() for timeouts or "first response wins" patterns

Promise.any() - First Success

// Try multiple sources, take first success
const data = await Promise.any([
  fetch('https://primary.api/data'),
  fetch('https://backup.api/data'),
  fetch('https://fallback.api/data')
]);
// Returns first successful response

// Only fails if ALL fail
await Promise.any([
  Promise.reject('A failed'),
  Promise.reject('B failed'),
  Promise.reject('C failed')
]);
// Throws: AggregateError with all rejection reasons

Use any() for redundant sources or fallback chains

Promise.allSettled() - Wait for All

// Get results regardless of success/failure
const results = await Promise.allSettled([
  Promise.resolve('success'),
  Promise.reject('error'),
  Promise.resolve('another')
]);

// results = [
//   { status: 'fulfilled', value: 'success' },
//   { status: 'rejected', reason: 'error' },
//   { status: 'fulfilled', value: 'another' }
// ]

// Filter successes
const successes = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

Use allSettled() when you need all results regardless of failures

Promisify Callback APIs

// Convert callback-based function to Promise
function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

// Usage
const readFile = promisify(fs.readFile);
const data = await readFile('file.txt', 'utf8');

Promisify wraps Node-style callbacks (err, result) into Promises

Retry with Exponential Backoff

async function retry(fn, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
    }
  }
}

// Usage
const data = await retry(
  () => fetch('/flaky-api'),
  3,    // 3 attempts
  1000  // 1s, 2s, 4s delays
);

Retry pattern with increasing delays between attempts

Common Mistakes

  • Forgetting to return in .then() chains (breaks the chain)
  • Not handling Promise rejections (.catch or try/catch)
  • Using Promise.all() when you need allSettled() behavior
  • Creating promises in loops without proper batching
  • Mixing async/await with .then() inconsistently

Interview Tips

  • Know the difference between all/race/any/allSettled
  • Implement Promise.all() from scratch
  • Explain the microtask queue and Promise execution order
  • Write a promisify function
  • Implement retry with exponential backoff

Practice Problems

Apply this concept by solving these 12 problems

Related Concepts