2024-01-12

setTimeout in a loop

While we're still at passing values to timer functions, let's take a little detour for a once common interview question: calling setTimeout in a loop. (I only hope it's not common any more. I never asked that anyone in the interview.)

A candidate would be presented with the first loop and a question what would happen. The correct answer is that it will print five times 5 in one-second breaks (more or less, as setTimeout cannot guarantee the exact time). People who don't fully understand hoisting, which happens when you use var, will correctly point to one-second breaks, but will miss the logging part. Ah, gotcha, junior!

for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i); // <-- by the execution time, this will be 5
    }, i * 1000);
}

The second part of the task is to fix it.

Back in the days, the usual solution was a closure that copies the value and uses it without interference from the outside.

for (var i = 0; i < 5; i++) {
    function closure(value) {
        setTimeout(function () {
            console.log(value);
        }, value * 1000)
    }
    
    closure(i);
}

Function method bind can be used as well.

for (var i = 0; i < 5; i++) {
    setTimeout(console.log.bind(null, i), i * 1000);
}

But, knowing about additional argument, we can just pass it this way (it's a primitive value, so it ends up being copied, not passed as a reference). The resulting code is actually the tersest of 'em all.

for (var i = 0; i < 5; i++) {
    setTimeout(console.log, i * 1000, i);
}

The life, however, wrote a new chapter to this hectic story. While var is hoisted, let and const are not. They are block-scoped and this somehow results in each iteration having access to the value as per its iteration. So, by swapping var to let, we make the original snippet working.

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i)
    }, i * 1_000);
}

And that's it for today. Thank you for coming to my tech talk.