Async loops, and why they fail! Part 2

More problems with async calls and loops in JavaScript; how to handle mapping and filtering.

Federico Kereki
DailyJS
5 min readJan 14, 2021

--

Photo by Tine Ivanič on Unsplash

In the first article in this series, “Async loops, and why they fail! — Part 1”, we saw that using common higher order functions like forEach() or reduce() wouldn’t work as expected with async functions. Similar problems apply to map() and filter() so let’s continue our work by dealing with them.

Mapping — a manual solution

If you try to directly use .map() with async functions, the result won’t be what you’d probably expect.

The output would be as follows.

19:58:20.008 START -- using map(...) 
19:58:20.009 END
Result of map: [
Promise { <pending> },
Promise { <pending> },
Promise { <pending> },
Promise { <pending> },
Promise { <pending> }
]

19:58:21.010 data #1
19:58:23.011 data #3
19:58:25.010 data #5
19:58:28.013 data #8

What’s this? We get the START message, and then END, and this is logical because .map() isn’t async-aware. The result of mapping is (correctly) an array, but instead of values, its contents are promises — one for each remote call. After logging that array, the results of the individual fake calls start coming in (and note that the 2nd call failed, as we designed, so we only get to see the returned values for the other four calls) but you still don’t get an array of values as a result.

What can we do? There are two solutions; we’ll see one here, and the other in the next section. We could just accept that the mapped array is made of promises, and use Promise.allSettled() to wait until all of them are resolved or rejected. However, that will still be an array of promises, so a further map() will be required to extract the values.

Now the output is as follows — first we get the array of (awaited) promises, and then the result of the final mapping, to extract the values. We get an undefined result for the failed call; we could easily produce any other result by testing the value of status.

After awaiting:  [ 
{ status: 'fulfilled', value: 'data #1' },
{ status: 'rejected', reason: 'FAILED' },
{ status: 'fulfilled', value: 'data #3' },
{ status: 'fulfilled', value: 'data #5' },
{ status: 'fulfilled', value: 'data #8' }
]
Final: [ 'data #1', undefined, 'data #3', 'data #5', 'data #8' ]

This works… but depends on the developer to do much work, first awaiting the array of promises, and then using mapping to extract the wanted values — couldn’t we do something like in the first article, and produce a .mapAsync() method that would take care of everything? Let’s do that!

Mapping — a better solution

After all the work we did in the previous section, coding a .mapAsync() method is simplicity itself. What did we end up doing? We first used map(), followed by waiting for all promises to be settled, and we finished by doing a new mapping to extract the values. We can put all of that together in a single (longish) line!

This is exactly what we did above, so we can just proceed to testing it.

Results are the same for the method and the function.

20:24:57.971 START -- using .mapAsync(...) method 
20:24:57.974 Calling - v=1 i=0 a=[1,2,3,5,8]
20:24:57.975 Calling - v=2 i=1 a=[1,2,3,5,8]
20:24:57.975 Calling - v=3 i=2 a=[1,2,3,5,8]
20:24:57.975 Calling - v=5 i=3 a=[1,2,3,5,8]
20:24:57.975 Calling - v=8 i=4 a=[1,2,3,5,8]
20:24:58.977 Success - data #1
20:24:59.977 Failure - error
20:25:00.976 Success - data #3
20:25:02.976 Success - data #5
20:25:05.979 Success - data #8
20:25:05.979 END -- [data #1,,data #3,data #5,data #8]

After the START message, we see that the five calls went out in parallel. Following that, we get the five results: one failure (the 2nd, as planned) and four successes. The final result is an array with five elements: the four successful results, and an undefined value for the failed call.

As in the previous article, in all cases we’ll be giving two implementations for each function — one adding to the Array.prototype (though this practice is not usually recommended…) and one as an independent function. You may pick whichever you like best.

We managed to implement an async- and promise-aware version of .map(); let’s do the same now for filtering.

Filtering

How can we implement filtering? We could do something along the lines of .forEachAsync() as in the first part of this article, but we can do better by using the .mapAsync() function we just wrote. The idea is simple: first apply mapping to the original array to produce a list of boolean values — if an output value is true (false) it means that the original array corresponding value should be included in (excluded from) the output. This leads to another one-liner solution.

The harder part to understand is how we use .filter() on the original array, but using the mapped array’s values for filtering. Let’s see an example of the usage of our new method; we’ll filter the original array to only keep odd values. For variety, now two calls will fail.

The results of this run are as follows.

20:39:15.237 START -- using .filterAsync(...) method 
20:39:15.241 Calling - v=1 i=0 a=[1,2,3,5,8]
20:39:15.241 Calling - v=2 i=1 a=[1,2,3,5,8]
20:39:15.241 Calling - v=3 i=2 a=[1,2,3,5,8]
20:39:15.241 Calling - v=5 i=3 a=[1,2,3,5,8]
20:39:15.241 Calling - v=8 i=4 a=[1,2,3,5,8]
20:39:16.244 Success - true
20:39:17.243 Failure - error
20:39:18.243 Failure - error
20:39:20.243 Success - true
20:39:23.242 Success - false
20:39:23.242 END -- [1,5]

All calls went out in parallel, and soon later the results started coming in — with two failures, as planned. The final filtering picks just the values 1 and 5; the filter returned false for 8, and failed for 2 and 3. All is OK!

Summary

In this second article, we continued our study of troublesome async loops in JavaScript, and developed alternative implementations for map() and filter(). In the next part we’ll be providing alternatives for some() and every(), which also don’t mix well with async calls and promises.

References

This article is partially based on Chapter 6, “Programming Declaratively — A Better Style” of my “Mastering JavaScript Functional Programming” book, for Packt; some code differs from the book.

Check MDN for the description of array.map() and array.filter().

Code for all articles in the series is available at my repository.

Don’t miss the other articles in the series:

--

--

Federico Kereki
DailyJS

Computer Systems Engineer, MSc in Education, Subject Matter Expert at Globant