A Guide to Asynchronous Array Iterator Functions

For Map, Reduce, Filter, Every, and Some

By Michael Chang

March 23rd, 2021

image

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?

Resources



Continue Learning