Make Array.forEach synchronous even with an asynchronous body

Array iterators in JavaScript are beautiful things, if you don't know about them read up on them here.

Array.forEach is handy for doing something to each item in an array synchronously and in order. So when forEach returns you can be sure that "something" was done to every item in the array.

Unless...

Unless "something" is an asynchronous call.

function myAsyncFunction(item) {
  // doAsyncStuff...
}

[1, 2, 3].forEach(item => {
  myAsyncFunction(item);
});

In this case all the asynchronous calls are made in order, but forEach has no way of knowing when the calls complete. If your calls are promises you can use Promise.all to wait for all of the promises are resolved.

But what if you want to make sure each iteration completes before the next is run? Then you are out of luck with Array.forEach. In my case, I wanted to iterate over an array of objects and store them in a database. In case objects with the same id occurs more than once, I wanted to get the object from the database and append to it. If the iteration was run in parallel I would risk overwriting the objects or accidentally duplicating them in the database.

Fortunately a colleague pointed me to a different Array iterator - Array.reduce. To quote MDN:

The reduce() method applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.

Does not look like a perfect fit for this problem until you realize that "accumulator" is not restricted to a primitive and the "function" can pretty much manipulate this value any way it wants - including building a whole new object.

The syntax of reduce is:

Array.reduce((currentValue, arrayItem) => {
  // Update currentValue here based on the arrayItem and return it as the new currentValue for the next iteration
}, initialValue);

initialValue is the first currentValue that will be passed to the manipulator function along with the first item in the array.

So how do we apply this to the problem this post is actually about? Again, assuming myAsyncFunction returns a promise:

[1, 2, 3].reduce((promiseChain, arrayItem) =>
  promiseChain.then(() => myAsyncFunction(arrayItem)), Promise.resolve());

So what does this do? It starts by passing Promise.resolve() and 1 (first array item) into the manipulator. In each iteration a new .then() with a call to myAsyncFunction is added to then end of Promise.resolve(). So it ends up calling:

Promise.resolve().then(myAsyncFunction(1)).then(myAsyncFunction(2)).then(myAsyncFunction(3));

Exactly what we wanted - a forEach that waits for an async call to finish for each item before proceeding to the next.

comments powered by Disqus
Find me on Mastodon