From Zero to AI

Lesson 2.4: Promise.all and Promise.race

Duration: 50 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Run multiple Promises in parallel using Promise.all()
  • Handle the first completed Promise with Promise.race()
  • Choose the right method for different scenarios
  • Understand Promise.allSettled() and Promise.any()
  • Implement common patterns like timeouts and fallbacks

Why Combine Promises?

Often, you need to perform multiple async operations:

  1. Sequentially: One after another (use chaining)
  2. In Parallel: All at once, wait for all (use Promise.all)
  3. Racing: All at once, use the first result (use Promise.race)
// Sequential - slow (3 seconds total)
const user = await fetchUser();      // 1 second
const posts = await fetchPosts();    // 1 second
const comments = await fetchComments(); // 1 second

// Parallel - fast (1 second total if independent)
const [user, posts, comments] = await Promise.all([
  fetchUser(),      // 1 second
  fetchPosts(),     // 1 second (runs simultaneously)
  fetchComments(),  // 1 second (runs simultaneously)
]);

Promise.all()

Promise.all() takes an array of Promises and returns a new Promise that:

  • Resolves when ALL input Promises resolve
  • Rejects immediately if ANY Promise rejects
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // [1, 2, 3]
});

Basic Example

function delay(ms: number, value: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

console.log('Starting...');
const startTime = Date.now();

Promise.all([delay(1000, 'First'), delay(2000, 'Second'), delay(1500, 'Third')]).then((results) => {
  const elapsed = Date.now() - startTime;
  console.log(`Results: ${results}`); // ["First", "Second", "Third"]
  console.log(`Time: ${elapsed}ms`); // ~2000ms (not 4500ms!)
});

The total time is determined by the slowest Promise, not the sum.

interface User {
  id: number;
  name: string;
}

interface UserProfile {
  bio: string;
}

interface UserSettings {
  theme: string;
  notifications: boolean;
}

function fetchUser(id: number): Promise<User> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: 'Alice' }), 500);
  });
}

function fetchProfile(userId: number): Promise<UserProfile> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ bio: 'TypeScript developer' }), 700);
  });
}

function fetchSettings(userId: number): Promise<UserSettings> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ theme: 'dark', notifications: true }), 600);
  });
}

// Fetch all user data in parallel
Promise.all([fetchUser(1), fetchProfile(1), fetchSettings(1)])
  .then(([user, profile, settings]) => {
    console.log('User:', user.name);
    console.log('Bio:', profile.bio);
    console.log('Theme:', settings.theme);
  })
  .catch((error) => {
    console.error('Failed to load user data:', error.message);
  });

Error Handling with Promise.all()

If any Promise rejects, the entire Promise.all() rejects immediately:

const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject(new Error('Failed!'));
const promise3 = Promise.resolve('Also success');

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log('All resolved:', values); // Never runs
  })
  .catch((error) => {
    console.log('One failed:', error.message); // "One failed: Failed!"
  });

Important: The other Promises continue running, but their results are discarded.

TypeScript Typing

TypeScript infers the return type based on input Promises:

// TypeScript infers: Promise<[string, number, boolean]>
const result = Promise.all([Promise.resolve('hello'), Promise.resolve(42), Promise.resolve(true)]);

result.then(([str, num, bool]) => {
  // str: string, num: number, bool: boolean
  console.log(str.toUpperCase()); // TypeScript knows it's a string
});

Promise.race()

Promise.race() returns a Promise that settles as soon as any input Promise settles:

const fast = delay(100, 'Fast');
const slow = delay(500, 'Slow');

Promise.race([fast, slow]).then((winner) => {
  console.log('Winner:', winner); // "Winner: Fast"
});

Racing with Errors

The first Promise to settle wins, whether it resolves or rejects:

const success = delay(200, 'Success');
const failure = new Promise<string>((_, reject) => {
  setTimeout(() => reject(new Error('Failed')), 100);
});

Promise.race([success, failure])
  .then((value) => console.log('Resolved:', value))
  .catch((error) => console.log('Rejected:', error.message));
// Output: "Rejected: Failed" (failure completes first)

Practical Use Case: Timeout

function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Operation timed out after ${timeoutMs}ms`));
    }, timeoutMs);
  });

  return Promise.race([promise, timeout]);
}

// Usage
const slowFetch = new Promise<string>((resolve) => {
  setTimeout(() => resolve('Data'), 5000);
});

fetchWithTimeout(slowFetch, 2000)
  .then((data) => console.log('Data:', data))
  .catch((error) => console.log('Error:', error.message));
// Output: "Error: Operation timed out after 2000ms"

Practical Use Case: First Available Server

interface ServerResponse {
  server: string;
  data: string;
}

function fetchFromServer(server: string, delay: number): Promise<ServerResponse> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ server, data: `Data from ${server}` });
    }, delay);
  });
}

// Race multiple servers, use the fastest
Promise.race([
  fetchFromServer('US', 200),
  fetchFromServer('EU', 150),
  fetchFromServer('Asia', 300),
]).then((response) => {
  console.log(`${response.server} responded first!`);
  console.log('Data:', response.data);
});
// Output: "EU responded first!"

Promise.allSettled()

Promise.allSettled() waits for all Promises to settle (resolve or reject) and returns their results:

const promises = [
  Promise.resolve('Success 1'),
  Promise.reject(new Error('Error 2')),
  Promise.resolve('Success 3'),
];

Promise.allSettled(promises).then((results) => {
  console.log(results);
  // [
  //   { status: "fulfilled", value: "Success 1" },
  //   { status: "rejected", reason: Error: Error 2 },
  //   { status: "fulfilled", value: "Success 3" }
  // ]
});

When to Use allSettled()

Use it when you want all results, even if some fail:

interface FetchResult {
  url: string;
  data?: string;
  error?: string;
}

async function fetchMultipleUrls(urls: string[]): Promise<FetchResult[]> {
  const promises = urls.map((url) =>
    fetch(url)
      .then((response) => response.text())
      .then((data) => ({ url, data }))
      .catch((error) => ({ url, error: error.message }))
  );

  const results = await Promise.allSettled(promises);

  return results.map((result, index) => {
    if (result.status === 'fulfilled') {
      return result.value;
    } else {
      return { url: urls[index], error: result.reason.message };
    }
  });
}

Filtering Results

interface User {
  id: number;
  name: string;
}

function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id % 2 === 0) {
        resolve({ id, name: `User ${id}` });
      } else {
        reject(new Error(`User ${id} not found`));
      }
    }, 100);
  });
}

const userIds = [1, 2, 3, 4, 5];

Promise.allSettled(userIds.map((id) => fetchUser(id))).then((results) => {
  const successful = results
    .filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
    .map((r) => r.value);

  const failed = results
    .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
    .map((r) => r.reason.message);

  console.log('Loaded users:', successful);
  console.log('Failed:', failed);
});

Promise.any()

Promise.any() returns the first fulfilled Promise, ignoring rejections:

const promises = [
  Promise.reject(new Error('Error 1')),
  new Promise((resolve) => setTimeout(() => resolve('Success!'), 100)),
  Promise.reject(new Error('Error 2')),
];

Promise.any(promises)
  .then((value) => {
    console.log('First success:', value); // "First success: Success!"
  })
  .catch((error) => {
    // AggregateError if ALL promises reject
    console.log('All failed:', error.errors);
  });

Difference from Promise.race()

Method Returns On First Reject
race() First settled (success or failure) Returns rejection
any() First fulfilled only Ignores, waits for success
const promises = [
  new Promise((_, reject) => setTimeout(() => reject(new Error('Fast fail')), 50)),
  new Promise((resolve) => setTimeout(() => resolve('Slow success'), 200)),
];

// race() returns the fast failure
Promise.race(promises)
  .then((v) => console.log('Race resolved:', v))
  .catch((e) => console.log('Race rejected:', e.message));
// Output: "Race rejected: Fast fail"

// any() waits for success
Promise.any(promises)
  .then((v) => console.log('Any resolved:', v))
  .catch((e) => console.log('Any rejected:', e.message));
// Output: "Any resolved: Slow success"

Use Case: Multiple Fallback Sources

function fetchFromPrimary(): Promise<string> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Primary unavailable')), 100);
  });
}

function fetchFromBackup(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Data from backup'), 200);
  });
}

function fetchFromCache(): Promise<string> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Cache miss')), 50);
  });
}

Promise.any([fetchFromPrimary(), fetchFromBackup(), fetchFromCache()])
  .then((data) => {
    console.log('Got data:', data); // "Got data: Data from backup"
  })
  .catch((error) => {
    console.log('All sources failed:', error.errors);
  });

Comparison Table

Method Resolves When Rejects When
Promise.all() ALL fulfill ANY rejects
Promise.race() FIRST settles FIRST rejects
Promise.allSettled() ALL settle Never (always resolves)
Promise.any() FIRST fulfills ALL reject

Practical Patterns

Pattern 1: Parallel with Timeout

async function fetchAllWithTimeout<T>(promises: Promise<T>[], timeoutMs: number): Promise<T[]> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), timeoutMs);
  });

  return Promise.race([Promise.all(promises), timeout]);
}

// Usage
fetchAllWithTimeout([fetchUser(1), fetchProfile(1), fetchSettings(1)], 3000)
  .then(([user, profile, settings]) => {
    console.log('All data loaded');
  })
  .catch((error) => {
    console.log('Failed or timed out:', error.message);
  });

Pattern 2: Batch Processing

async function processBatch<T, R>(
  items: T[],
  processor: (item: T) => Promise<R>,
  batchSize: number
): Promise<R[]> {
  const results: R[] = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(processor));
    results.push(...batchResults);
    console.log(`Processed ${Math.min(i + batchSize, items.length)}/${items.length}`);
  }

  return results;
}

// Usage
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

processBatch(userIds, fetchUser, 3).then((users) => {
  console.log('All users:', users.length);
});

Pattern 3: First Success with Fallback

async function fetchWithFallback<T>(
  primary: () => Promise<T>,
  fallback: () => Promise<T>
): Promise<T> {
  try {
    return await primary();
  } catch {
    console.log('Primary failed, trying fallback...');
    return fallback();
  }
}

// Or using Promise.any for parallel fallback
function fetchWithParallelFallback<T>(sources: Array<() => Promise<T>>): Promise<T> {
  return Promise.any(sources.map((source) => source()));
}

Pattern 4: Load with Progress

interface ProgressUpdate {
  completed: number;
  total: number;
  percentage: number;
}

async function loadWithProgress<T>(
  promises: Promise<T>[],
  onProgress: (update: ProgressUpdate) => void
): Promise<T[]> {
  let completed = 0;
  const total = promises.length;

  const wrapped = promises.map((p) =>
    p.then((result) => {
      completed++;
      onProgress({
        completed,
        total,
        percentage: Math.round((completed / total) * 100),
      });
      return result;
    })
  );

  return Promise.all(wrapped);
}

// Usage
loadWithProgress([fetchUser(1), fetchProfile(1), fetchSettings(1)], (progress) => {
  console.log(`Loading: ${progress.percentage}%`);
}).then((results) => {
  console.log('All loaded!');
});

Exercises

Exercise 1: Parallel Fetch

Fetch three pieces of data in parallel and log the total time:

function fetchA(): Promise<string> {
  return new Promise((resolve) => setTimeout(() => resolve('A'), 1000));
}

function fetchB(): Promise<string> {
  return new Promise((resolve) => setTimeout(() => resolve('B'), 1500));
}

function fetchC(): Promise<string> {
  return new Promise((resolve) => setTimeout(() => resolve('C'), 800));
}
Solution
const start = Date.now();

Promise.all([fetchA(), fetchB(), fetchC()]).then(([a, b, c]) => {
  const elapsed = Date.now() - start;
  console.log('Results:', a, b, c); // "Results: A B C"
  console.log(`Time: ${elapsed}ms`); // ~1500ms
});

Exercise 2: Implement Timeout

Create a function that races a Promise against a timeout:

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {}

// Test
const slow = new Promise<string>((resolve) => {
  setTimeout(() => resolve('Done'), 5000);
});

withTimeout(slow, 1000)
  .then((v) => console.log('Resolved:', v))
  .catch((e) => console.log('Error:', e.message));
// Should print "Error: Timeout" after 1 second
Solution
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });

  return Promise.race([promise, timeout]);
}

Exercise 3: Get All Results

Use Promise.allSettled() to fetch multiple users and separate successes from failures:

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 3) {
        reject(new Error('User 3 not found'));
      } else {
        resolve({ id, name: `User ${id}` });
      }
    }, 100);
  });
}

const ids = [1, 2, 3, 4, 5];
Solution
Promise.allSettled(ids.map((id) => fetchUser(id))).then((results) => {
  const successful = results
    .filter(
      (r): r is PromiseFulfilledResult<{ id: number; name: string }> => r.status === 'fulfilled'
    )
    .map((r) => r.value);

  const failed = results
    .map((r, i) => (r.status === 'rejected' ? ids[i] : null))
    .filter((id): id is number => id !== null);

  console.log('Loaded:', successful);
  console.log('Failed IDs:', failed);
});

// Output:
// Loaded: [{ id: 1, name: "User 1" }, { id: 2, name: "User 2" }, ...]
// Failed IDs: [3]

Exercise 4: First Available

Use Promise.any() to get data from the first available source:

function fetchFromServerA(): Promise<string> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Server A down')), 100);
  });
}

function fetchFromServerB(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Data from B'), 300);
  });
}

function fetchFromServerC(): Promise<string> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Server C down')), 200);
  });
}
Solution
Promise.any([fetchFromServerA(), fetchFromServerB(), fetchFromServerC()])
  .then((data) => {
    console.log('Got data:', data); // "Got data: Data from B"
  })
  .catch((error) => {
    console.log('All servers failed:', error.errors);
  });

Key Takeaways

  1. Promise.all(): Run in parallel, wait for ALL, fail on ANY error
  2. Promise.race(): Run in parallel, return FIRST to settle
  3. Promise.allSettled(): Run in parallel, wait for ALL, never fails
  4. Promise.any(): Run in parallel, return FIRST success, fail only if ALL fail
  5. Use parallel execution for independent operations to save time
  6. Use race for timeouts and first-available scenarios
  7. Use allSettled when you need all results regardless of errors
  8. Use any when you want the first success with automatic fallback

Resources

Resource Type Description
MDN: Promise.all() Documentation Promise.all reference
MDN: Promise.race() Documentation Promise.race reference
MDN: Promise.allSettled() Documentation Promise.allSettled reference
MDN: Promise.any() Documentation Promise.any reference

Next Lesson

Now let us put everything together in a practical exercise: building a Sequential Data Loader that fetches related data from an API.

Continue to Lesson 2.5: Practice - Sequential Requests