JavaScript
Async Programming
Promise.all
map Function
Concurrency

map with async vs promise.all

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

When working with arrays of asynchronous operations in JavaScript, developers often reach for .map() with an async callback. The key question is what happens to the array of promises that .map() returns and how Promise.all() fits into the picture. This article breaks down exactly how these two tools interact, when to combine them, and the mistakes developers make when using them separately.

What .map() with async Actually Returns

The Array.prototype.map() method calls a function on every element and collects the return values into a new array. When that function is async, each call returns a Promise. The result is an array of Promises, not an array of resolved values.

javascript
1const userIds = [1, 2, 3];
2
3const promises = userIds.map(async (id) => {
4  const response = await fetch(`/api/users/${id}`);
5  return response.json();
6});
7
8console.log(promises); // [Promise, Promise, Promise]

At this point, all three fetch requests have been fired off concurrently. However, promises is just an array of unresolved Promise objects. You cannot use the results directly.

Using Promise.all() to Collect Results

Promise.all() takes an array of promises and returns a single promise that resolves when all of them have resolved. The resolved value is an array of results in the same order as the input.

javascript
1const userIds = [1, 2, 3];
2
3const users = await Promise.all(
4  userIds.map(async (id) => {
5    const response = await fetch(`/api/users/${id}`);
6    return response.json();
7  })
8);
9
10console.log(users); // [{name: "Alice"}, {name: "Bob"}, {name: "Charlie"}]

This is the standard pattern. The .map() fires all requests concurrently, and Promise.all() waits for every one of them to finish before giving you the results array.

What Happens Without Promise.all()

If you use .map() with async but never await the resulting promises, the operations still run. They just run in a "fire and forget" manner. This can lead to unhandled promise rejections and race conditions:

javascript
1// Dangerous: no await, no error handling
2userIds.map(async (id) => {
3  await updateUser(id); // runs, but nobody is listening
4});
5
6console.log("Done"); // prints immediately, before any updates finish

The console.log executes before any of the async operations complete because nothing is waiting on the promises.

for...of with await Runs Sequentially

A common alternative is using for...of with await, but this runs each operation one at a time:

javascript
1const users = [];
2for (const id of userIds) {
3  const response = await fetch(`/api/users/${id}`);
4  users.push(await response.json());
5}

Each request waits for the previous one to finish. If each request takes 200ms, three requests take 600ms total instead of roughly 200ms with the concurrent approach. Use this pattern only when operations must happen in sequence, for example when each request depends on the result of the previous one.

Error Handling Differences

Promise.all() has a fast-fail behavior. If any single promise rejects, the entire Promise.all() rejects immediately with that error. The other promises continue executing in the background, but their results are discarded.

javascript
1try {
2  const results = await Promise.all(
3    userIds.map(async (id) => {
4      const res = await fetch(`/api/users/${id}`);
5      if (!res.ok) throw new Error(`Failed for user ${id}`);
6      return res.json();
7    })
8  );
9} catch (err) {
10  console.error(err.message); // "Failed for user 2"
11  // results from user 1 and 3 are lost
12}

If you need all results regardless of individual failures, use Promise.allSettled() instead:

javascript
1const results = await Promise.allSettled(
2  userIds.map(async (id) => {
3    const res = await fetch(`/api/users/${id}`);
4    return res.json();
5  })
6);
7
8results.forEach((result) => {
9  if (result.status === "fulfilled") {
10    console.log(result.value);
11  } else {
12    console.error(result.reason);
13  }
14});

Controlling Concurrency

One limitation of Promise.all() with .map() is that it fires all operations at once. For large arrays, this can overwhelm a server or hit rate limits. You can batch operations manually:

javascript
1async function mapWithConcurrency(items, fn, concurrency = 5) {
2  const results = [];
3  for (let i = 0; i < items.length; i += concurrency) {
4    const batch = items.slice(i, i + concurrency);
5    const batchResults = await Promise.all(batch.map(fn));
6    results.push(...batchResults);
7  }
8  return results;
9}
10
11const users = await mapWithConcurrency(userIds, async (id) => {
12  const res = await fetch(`/api/users/${id}`);
13  return res.json();
14}, 3);

Common Pitfalls

  • Forgetting Promise.all(): Using .map(async ...) alone gives you an array of promises, not results. Always wrap with Promise.all() if you need the values.
  • Using forEach with async: Array.forEach() ignores return values entirely. Async callbacks inside forEach create fire-and-forget promises with no way to catch errors.
  • Unbounded concurrency: Mapping over thousands of items with Promise.all() launches all requests simultaneously. Use batching or a library like p-limit to control concurrency.
  • Assuming order of execution: While Promise.all() preserves the order of results, the actual execution order of the async operations is non-deterministic.

Summary

Use .map() with an async callback to fire multiple asynchronous operations concurrently. Wrap the result with Promise.all() to await all of them and collect the resolved values. Use Promise.allSettled() when you need results from every operation regardless of failures. Use sequential for...of only when operations depend on each other. For large arrays, add concurrency control to avoid overwhelming downstream services.


Course illustration
Course illustration

All Rights Reserved.