My Journey
JavaScript's built in Array map, reduce, filter, every, and some iterator functions are second nature to me. But recently, I needed to build some functionality that required asynchronous callback functions. Let’s see how we can accomplish this.
Map
map iterates through the array, calculates the result for each element using the provided callback function, and returns a new array of results.
[1, 2, 3].map(i => i * 2);
// [2, 4, 6]
If the provided function returns a promise, map unfortunately will not resolve the promise.
[1, 2, 3].map(async i => i * 2);
// [Promise, Promise, Promise]
But resolving this is quite simple. Promise provides a built-in Promise.all function that will resolve all the promises in an array — the exact output we expected from map.
await Promise.all([1, 2, 3].map(async i => i * 2));
// [2, 4, 6]
Reduce
reduce iterates through the array and aggregates a single value based on the provided callback reducer function.
[1, 2, 3].map((acc, i) => acc + i, 0)
// 6
If the provided function returns a promise, reduce will error.
[1, 2, 3].reduce(async (acc, i) => {
return acc + i;
}, 0);
// Error
Why? Because acc is the response of the previous iteration, it is a promise itself — and you can’t aggregate on a promise. You will need to resolve acc first before aggregating.
[1, 2, 3].reduce(async (acc, i) => {
return (await acc) + i;
}, 0);
// 6
Filter
filter iterates through the array and returns a new array with only elements that pass the provided callback test function.
[1, 2, 3].filter(i => i < 3);
// [1, 2]
If the provided filter function returns a promise, filter will not work as expected and instead, will return the entire array. This is because the async function returns a promise but the filter function does not resolve the promise. Instead it directly uses the promise, which evaluates to true, causing the filter function include the element in the result.
[1, 2, 3].filter(async i => i < 3);
// [1, 2, 3]
Unfortunately, filter cannot support promises but you can indirectly achieve the result using map.
const array = [1, 2, 3];
const mapResult = await Promise.all(array.map(async i => i < 3));
array.filter((_, idx) => mapResult[idx]);
// [1, 2]
First, using the the asynchronous map method as mentioned before, we calculate whether the element passes the filter function test and resolve all the promises. This yields an array of booleans which can then be used to determine which elements in the original array should be included in the result.
Every / Some
every checks that all elements in an array passes the provided callback test function and some checks that at least one element in an array passes.
[1, 2, 3].every(i => i < 3);
// false
[1, 2, 3].some(i => i < 3);
// true
Just like with filter, if the provided test function returns a promise, every and some will not work as expected. Instead, they will always return true, because the unresolved promise will evaluate to true.
[1, 2, 3].every(async i => i < 3);
// true
[1, 2, 3].some(async i => i < 3);
// true
The solution is similar to that of filter’s — by using asynchronous map to evaluate the test and iterating through the results.
const array = [1, 2, 3];
const mapResult = await Promise.all(array.map(async i => i < 3));
array.every((_, idx) => mapResult[idx]);
// false
array.some((_, idx) => mapResult[idx]);
// true
The downside to this approach is that every element in the iteration will have their promise resolved. every and some can short-circuit and exist as soon as the first false and true result, respectively, is calculated, saving on computation time. To match this behavior, you have to iterate through the array and run the test function one at a time with a regular for loop.
const asyncFunction = async i => i < 3;
let everyResult = true;
for (let i of [1, 2, 3]) {
if (!(await asyncFunction(i))) {
everyResult = false;
break;
}
}
// everyResult = false
let someResult = false;
for (let i of [1, 2, 3]) {
if (await asyncFunction(i)) {
someResult = true;
break;
}
}
// someResult = true
Final Thoughts
map, reduce, filter, every, and some can all handle async callback functions with just a little bit of tweaking. While not covered here, other built in Array methods such as find, findIndex, forEach, and reduceRight can also handle async callback functions using the same methodology. Can you figure out how?