How to use reduce and forEach with async functions
Migsar Navarro
Posted on September 17, 2021
Cover photo by jens holm on Unsplash.
Array's prototype has a few different ways of iterate through arrays, but it can be tricky when you start using async functions, from simply not getting it working, in the case of forEach
, to getting unexpected results with reduce
.
The content of this post might be considered common sense for some, and it has already been mentioned in posts like these:
I think it comes handy to have it summarized in a single place and to have some opinionated discussion about what could be considered good practices.
Promises
The async/await
syntax is just another way to use promises, it was introduced because promises as handler's callbacks can create deeply nested logic that can be hard to reason about since scope becomes very important. With async/await
you can express the same flow in a more linear way and it has been incredibly popular.
I think that serious javascript developers should be comfortable using Promises, but that does not mean that I don't like this newer syntax. For some very specific cases like the ones presented here, I think it is easier to understand what is happening under-the-hood if Promises are used, but I won't use Promises in code because I think most often you'll find this syntax in the wild.
Loops
There are several different ways to iterate through arrays, we even have the for await...of
statement, you might get some lint errors for using one or another solution and it depends on the linter configuration.
I really like to use Array prototype's methods instead of for statements because I think it makes easier to understand the code and to move around blocks of code and refactor. I don't like to use lodash
unless some very specific behavior is needed, I prefer to use native es
functions whenever possible, but I am aware some projects with thousand of starts in Github still use lodash
.
At the end I think the most important consideration is convention, don't feel bad if you disagree and don't think one way is better than another just because it is used in a very popular project. Have a good conversation with the rest of the team, find the solution that better suits your mood and stick with it.
Array.prototype.forEach
Array.prototype.forEach
DOES NOT support async functions, it won't block execution and the result of the calculations inside of the Promises will be lost. There are a few options but I think the more elegant is to use Promise.all(arr.map(somePromise))
.
Array.prototype.reduce
Reduce does support async functions but there is a catch, the accumulator will now be a Promise, it completely makes sense since an async function ALWAYS return a Promise, but I feel it is not very intuitive and it can be hard to debug.
A very simple example
A simple example for Node.js, but should require minor modifications to be used in a browser.
const util = require('util');
const promiseFactory = (label) => (number) => new Promise((resolve) => {
console.debug(`${label}: ${number} was started!`);
setTimeout(() => {
console.log(`${label}: ${number} is about to be resolved!`);
resolve(number);
}, number * 1000);
});
(async function () {
// Example 1: use regular Array.forEach with Promises
const p1 = promiseFactory('Ex1');
console.log('Ex1: Let\'s try an async forEach:');
await [1, 2, 3, 4].forEach(async (i) => {
await p1(i);
});
console.log('Ex1: This should run at the end!');
// Example 2: Use Promise.all and map to achieve the same effect of an async Array.forEach
const p2 = promiseFactory('Ex2');
console.log('Ex2: Tweak for an async forEach:');
await Promise.all([5, 6, 7, 8].map(async (i) => {
await p2(i);
}));
console.log('Ex2: This should run after all promises!');
// Example 3: Use Array.reduce with Promises
const p3 = promiseFactory('Ex3');
console.log('Ex3: Reduce with promises');
const result3 = await [1, 2, 3, 4].reduce(async (acc, item) => {
return {
...acc,
[item]: await p3(item),
};
}, {});
console.log(`Ex3: ${util.inspect(result3, { depth: null })} is not what we expected!`);
// Example 4: Use await for accumulator in async Array.reduce
const p4 = promiseFactory('Ex4');
console.log('Ex4: Accumulator is a Promise!');
const result4 = await [1, 2, 3, 4].reduce(async (acc, item) => {
const resolvedAcc = await acc;
return {
...resolvedAcc,
[item]: await p4(item),
};
}, {});
console.log(`Ex4: ${util.inspect(result4, { depth: null })} is what we expected!`);
})();
Posted on September 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.