Async Map
Prompt
Implement an asyncMap function that transforms each element in an array using an asynchronous mapping function.
Unlike the standard Array.prototype.map() that works with synchronous operations, your function should handle asynchronous transformations and wait for all of them to complete.
The function should return a Promise which resolves to the mapped results.
Requirements:
- All mapping operations should run in parallel, not one after another.
- The result array must preserve the same order as the input, regardless of which async operation finishes first.
- The mapper callback should receive
(element, index, array)— just like the nativeArray.prototype.map(). - If any mapper call rejects, the returned Promise should reject with that error.
Example
const asyncTriple = async (x) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return x * 3;
};
const tripled = await asyncMap([1, 2, 3], asyncTriple);
console.log(tripled); // [3, 6, 9]Playground
Consider using Promise.all() to wait for all the
asynchronous operations to complete. This method takes an
array of promises and returns a single promise that
resolves when all promises in the array have resolved.
Remember that the order of results matters. Even though async operations may complete in any order, your function should ensure the results maintain the same order as the input array.
If any single mapper call throws or rejects, the entire
asyncMap should reject with that error — just like how
Promise.all() behaves. You don't need to handle
individual errors differently.
Solution
Explanation
If you've used Array.prototype.map() before, you know it takes a function, runs it on every element, and gives you a new array back. Simple.
But what if that function is async — like fetching data from an API or reading a file? Regular map doesn't wait for promises. It just gives you back an array of pending promises, which isn't what you want.
That's the problem asyncMap solves.
The solution
async function asyncMap(arr, asyncFn) {
const promises = arr.map((item, index, array) =>
asyncFn(item, index, array)
);
return Promise.all(promises);
}That's it — two lines. Let's walk through what's happening.
Step 1: Create an array of promises
const promises = arr.map((item, index, array) =>
asyncFn(item, index, array)
);We use the regular map to call asyncFn on every element. Since asyncFn is async, each call returns a Promise. So after this line, promises is an array of Promises — all of them are already running in parallel.
Notice we pass (item, index, array) to the mapper, just like the native map does. This way the caller can use the index or the original array if they need to.
Step 2: Wait for all of them
return Promise.all(promises);Promise.all takes an array of promises and waits for all of them to finish. When they're done, it gives you back an array of results — in the same order as the input. That last part is important: even if the third item finishes before the first, Promise.all still puts the results in the right order.
If any promise rejects, Promise.all immediately rejects with that error. This is exactly the behavior we want — if one mapping operation fails, the whole asyncMap fails.
Why this works
Let's say we call:
const result = await asyncMap(
[10, 20, 30],
async (x) => x * 2
);Here's what happens under the hood:
mapcalls the async function on each element →[Promise(20), Promise(40), Promise(60)]Promise.allwaits for all three to resolve- We get
[20, 40, 60]— same order as the input
What about errors?
await asyncMap([1, 2, 3], async (x) => {
if (x === 2) throw new Error('Boom');
return x;
});
// Rejects with Error: BoomWhen the mapper throws on x === 2, that promise rejects. Promise.all sees the rejection and immediately rejects the whole thing. The caller can catch it with try/catch or .catch().
Bonus: What if Promise.all didn't exist?
In an interview, you might be asked to solve this without Promise.all. Here's how you'd do it by hand:
function customPromiseAll(promises) {
const results = [];
let completed = 0;
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
results[index] = value;
completed++;
if (completed === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}The key trick here is results[index] = value — we store each result at its original index, not just push it to the end. This is what keeps the order correct even when promises finish in random order.