What's the problem with async functions inside array.forEach()?

Published: 22.11.2020 | 401 Words | 2 minutes
Tags: [ typescript javascript async ]

Consider the following program:

async function work() {
    const elements: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    elements.forEach(async element => {
        if(! await validate(element)) { console.log("[Error] Validation failed " + element )}
    })
    
    console.log("Continue with validated values")
}

async function validate(element:number): Promise<Boolean> {

    // The wait is problaby caused by a database lookup or backend requests
    await new Promise(r => setTimeout(r, 1000));
    return element < 9;
}

work();

The program has some input, in this case the elements array, and should validate each element of the array. The validation in this example is easy: The number should be lower than 9.

Because not all validations are that easy the validate function has a timer that models the time it would take to e.g. query a database or another system for the validation-task.

Take a moment to think about the expected output of the program. It should be like this:

[Error] Validation failed 9
[Error] Validation failed 10
Continue with validated values

but instead after running the code we get:

Continue with validated values
[Error] Validation failed 9
[Error] Validation failed 10

The programm proceded before the valiation of the input was done!

Why is this happening?

Usually the forEach is iterating through the array and applies the specified function for each element. Now the function is async, on invocation it returns a Promise. In our program we use the await keyword to wait for this Promise to fulfil (more information about that). The forEach is simply not applying the await operator. It recieves back a Promise and the immediately calls the function for the next array element, until no elements are left. Then it returns and the next statement, "console.log("Continue with validated Values")", is executed before the input validation is done.

How to avoid this?

Simple, don't use forEach for async validation-task. Just use a normal for-loop or a for..of loop.

async function work() {
    const elements: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];


    for (const element of elements) {
        if(! await validate(element)) { console.log("[Error] Validation failed")}
    }
    
    console.log("Continue with validated values")
}

async function validate(element:number): Promise<Boolean> {

    // The wait is problaby caused by a database lookup or backend requests
    await new Promise(r => setTimeout(r, 1000));
    return element < 9;
}

work();

Now the output is as expected:

[Error] Validation failed 9
[Error] Validation failed 10
Continue with validated values
That's all ;)
Back to the top