The idea to write this article came up to me when a friend of mine asked me to wait on an asynchronous function result, in the map’s callback. I’ve explained to him how to solve the issue. After brief research, I’ve realized that it seems to be a pretty common problem so the next step was obvious for me, write it down and share the solution with others!
What is this article about?
In this article, I will share my thoughts on dealing with async functions in JavaScript collections. I’m going to explain it the way that works for me the best, that is by showing examples of code, which you can also find at my github repository.
We are going to be working with fake a API which returns users by ids in order to simulate an asynchronous call to real API and based on that we go through several examples where we gonna focus on:
- for loop,
- forEach,
- for … of,
- map,
- filter,
- reduce
To simplify and focus on the topic every example has the same main steps:
- start program,
- start measure time,
- call getUsers method,
- end measure time,
- execute tests,
Furthermore in a parallel method, counting to 10 was added to better show async operations.
Let’s code
Example 1
Iterating through callbacks using for loop
Test (line 23) expects to receive four users, but when execute this code (node async-cb.js) error will be returned. If we log the users, it will come out that the array is empty and that’s because we just iterate through all ids without waiting for a response. We can prove that by commenting out a test (line 23) and adding log in API callback (line 9 -11). Then we’ll actually receive data from our API and now the main issue is to wait for data from getUsers method which we will try to solve in the following steps.
Example 2
Iterating through promises using for loop
This example contains one more test (line 22) which basically checks the format of data returned from the getUsers method. If you execute this code, notice that this time tests passed. Additionally, you’ll see counting to 10 to mark that our call to API is async. Sum up adding promises which are invoking inside, for loop guarantee wait for data before executing any operation on it. The next question is what if we would like to use array methods like forEach, filter, map, reduce?
Let’s gonna see what happens.
Example 3
Iterating through promises using forEach
In this example, we’ve only replaced for loop on forEach but when you execute this code you’ll see that our tests failed. When you will log users returned from getUsers method, you would see that array is empty. Basically, we came back to the starting point- thanks to that, we found the base issue which is the same for filter, map, reduce methods which we gonna discuss next.
Empty users array is due to the fact that forEach itself doesn’t wait for callbacks instead iterate through all objects then returns empty users array because any response from API didn’t make at this time.
Now we’re going to focus on how to deal with this behavior and handle a different kind of operation on arrays that contain async functions in callbacks.
Before we move to the next example please try to execute this code
We barely replaced forEach with for…of, check the result and try to answer yourself why did that happen?
Example 4
Iterating through promises using map
Thanks to using the map, we don’t have to declare users’ array before iterating through ids, now the map itself returns users in accordance with how it works. Notice how to resolve all promises (line 10) is required. Many times lack of it, causes an issue but why is it required? The map itself doesn’t return data. Instead of an array of promises , it’s related with the same fact as in forEach. The map does not wait until each promise is resolved, it returns a pending promise.
I would like to draw your attention to getUsers execute time, let’s compare it with for loop
Whence that difference comes from? Each of this method executes four requests for the same sets of users id, to be more precise our API request is setTimeout method which after ~500 ms (more or less because setTimeout guarantee that executes callback at least after 500ms not exactly in 500ms, this is not a topic of this article so to get the answer why is that I encourage to get more familiar with event loop) returns user. Back to the question, the difference came from the fact that map set all four request one after another without waiting for each response.
after that we get our users (Promise.all), in time which is equal to the longest promise request which in this case is the same for all requests and is equal ~500ms, whereas for loop works in that way:
Example 5
Iterating through promises using filter
In this example, our task is to only get users which are adults. By knowing how to get each user from our API, to reach our goal we only have to filter these users by age (line 13). It is also possible to achieve our goal in a more functional way by introducing pipe method. In this example it will be pipeAsync. Let’s check how code will look like after changes
Example 6
Iterating through promises using reduce
Now our task is to sum users account balances. If you try to execute the code you will receive an error. Let’s add log in line 18 to check sum
[object Promise]40000 where does it come from? It turns out that the 40000 value is the account balance of the last user (you can check that in lib/api.js line 5), but why do we receive [object Promise] is related to an async key keyword in reduce’s callback (line 6). Every async function return promise, so in our case prev variable isn’t equal to a specific value but a promise. In order to get the actual value we have to use the await keyword and resolve promise, let’s do that
Now tests passed, but I want to pay your attention to execute time
Why is it over 2000ms again? It looks like we’re back to the following execution order
and that’s true, because of the introduced variable prevSum (line 3) each callback has to wait for a previous value in order to make an API request. To fix that we only need to swap lines 3 and 4, so let’s do that and check the result
We’ve improved the performance of our code by 75% just by swapping two lines! This shows how important is to understand well asynchronous programming.
What’s next?
I encourage you to check my repository on github where I’ve prepared a few exercises for you to improve your confidence with async/await.
I hope that you found something for you in this article and the exercises turned out usefull!
Twitter: k_wdowik