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.
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.
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:
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:
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.
If you need all results regardless of individual failures, use Promise.allSettled() instead:
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:
Common Pitfalls
- Forgetting
Promise.all(): Using.map(async ...)alone gives you an array of promises, not results. Always wrap withPromise.all()if you need the values. - Using
forEachwith async:Array.forEach()ignores return values entirely. Async callbacks insideforEachcreate 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 likep-limitto 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.

