Friday 18 May 2018

Foreach Async Gotcha

Today I came up with an interesting gotcha with async/await. I transformed a normal for loop containing await calls into an Array.prototype.forEach call, and realised that it was not working properly. Let's see:

Let's say that we have this nice async loop:

//asynchrnous function returning a Promise
function formatTextAsync(txt){
    return new Promise((res, rej) => {
        setTimeout(()=> res(txt.toUpperCase()),
         2000);
    });
}

async function loop1(){
    console.log("loop1 start");
    let items = ["bonjour", "hi", "hola"];
    for (item of items){
        console.log(Date.now() + " input: " + item);
        item = await formatTextAsync(item);
        console.log(Date.now() + " output: " + item);
    }
    console.log("loop1 end");
}

console.log("before main");
loop1();
//loop2();
//loop3();
console.log("after main");

// before main
// loop1 start
// 1526678289508 input: bonjour
// after main
// 1526678291514 output: BONJOUR
// 1526678291515 input: hi
// 1526678293518 output: HI
// 1526678293518 input: hola
// 1526678295521 output: HOLA
// loop1 end

If we decide to move the loop body into an Array.prototype.forEach method call, the output is quite different:

async function loop2(){
    console.log("loop2 start");
    ["bonjour", "hi", "hola"].forEach(async (item) => {
        console.log(Date.now() + " input: " + item);
        item = await formatTextAsync(item);
        console.log(Date.now() + " output: " + item);
    });
     console.log("loop2 end");
}

// before main
// loop2 start
// 1526678543105 input: bonjour
// 1526678543109 input: hi
// 1526678543109 input: hola
// loop2 end
// after main
// 1526678545109 output: BONJOUR
// 1526678545111 output: HI
// 1526678545111 output: HOLA

So in the second code, the next iteration is launched without waiting for the async call in the previous iteration to complete. This means that the async calls end up running in parallel rather than in sequence.

The explanation for this behaviour is quite simple. We've moved the loop body containing the async call into a new function that we've marked as async, but, is anyone awaiting for this new function?, the answer is No, so the next iteration is started without waiting.

We can guess that the implementation of Array.forEach is something along these lines:

Array.prototype.forEach = function (fn) {
    for (let item of this) { fn(item) }
}

In order to get the correct async behaviour that we have when using a normal loop, we would need something like this:

Array.prototype.forEachAsync = async function (fn) {
    for (let item of this) { await fn(item) }
}

No comments:

Post a Comment