forEach/map and async don’t go hand in hand
At times, we often tend to pass async functions with forEach and map functions.
For ex:
[1,2,3,4,5].forEach(async (n) => setTimeout(console.log(n), 1000));
Expected: It is expected that all numbers from 1..5 are printed one after the other with a gap of 1 second after printing each line.
Outcome: All numbers from 1..5 are printed one after the other but there is no gap of 1 second after printing each line.
Why does this happen?
Look at this javascipt function:
async function doubleOf(n) {
return n*2;
}
Expected: At times one can mistakenly think that this function takes in a number and returns a number.
Outcome: This is actually returning a Promise that resolves to a number.
If we write a Typescript equivalent of this function with strict types, it will make things clear.
The following code won’t compile:
async function doubleOf(n: number): number {
return n*2;
}
as this is an async function and async functions only return objects of type Promise that could resolve to a value (including null). An async function does not return values like numbers, strings, etc.
Correct version would be:
async function doubleOf(n: number): Promise<number> {
return n*2;
}
Reading the above function would compile just fine and make it clear to the reader that this function takes in a number and returns a promise.
Don’t be fooled by syntactic sugar provided by async-await. If we wrote pure promises without using async, the above function would look like:
function doubleOf(n) {
return new Promise((resolve) => resolve(n*2));
}
Or typescript equivalent:
function doubleOf(n: number): Promise<number> {
return new Promise((resolve) => resolve(n*2));
}
Clearer picture:
- We have
doubleOf
function that takes in a number and returns a number. Plain old javascript. - We have
doubleOfOldWay
function that takes in a number and returns a promise that resolves to a number. - We have
doubleOfNewWay
, an async function that takes in a number and seems like it returns a number, but it actually returns a promise that resolves to a number just likedoubleOfOldWay
function. doubleOfOldWay
anddoubleOfNewWay
functions are exactly the same.- And hence, when we try to execute a multiply operation on values returned by
doubleOfOldWay
anddoubleOfNewWay
functions, the result wasNaN
, as we cannot multiple promises (obviously!). - To multiply
doubleOfOldWay
anddoubleOfNewWay
:
(await doubleOfOldWay(5)) * (await doubleOfNewWay(6))
this will result in 120
So back to our initial example:
[1,2,3,4,5].forEach(async (n) => setTimeout(console.log(n), 1000));
This line of code creates 5 promises and each promise waits for 1 second asynchronously (independent of each other), resolves individually irrespective of the previous number, and prints the respective number. And hence, no wait time was noticed.
The most correct way to implement what we expect from this forEach function is using a simple for loop:
for(const number of [1,2,3,4,5]) {
console.log(number);
await new Promise(resolve => setTimeout(resolve, 1000)); // Sleep for "atleast" 1 second
}
One more simple example:
[1,2,3,4,5].map(async (n) => n*2);
By now it should be clear that the output of this line of code will be:
(5) [Promise, Promise, Promise, Promise, Promise]
0: Promise {<fulfilled>: 2}
1: Promise {<fulfilled>: 4}
2: Promise {<fulfilled>: 6}
3: Promise {<fulfilled>: 8}
4: Promise {<fulfilled>: 10}
an array of promises that resolve numbers, and not an array of numbers.
To get a list of double of each number, what we can do is:
await Promise.all([1,2,3,4,5].map(async (n) => n*2));
this will result in:
[2, 4, 6, 8, 10]