Interview Questions: setTimeout and Closures
Prerequisite: If you donβt understand how Closures work, I highly recommend reading this first:
Basic Definition: setTimeout
The setTimeout() function executes a provided function or code snippet once after a specified delay (in milliseconds).
Syntax:
callbackFunction: The function to run after the delay.delayInMilliseconds: The time, in milliseconds, to wait before executing the function.
Let's look at a simple example:
| index.js | |
|---|---|
If we run this program, what do you expect to see in the console?
You might initially expect setTimeout() to pause execution for 3 seconds, print i, and then print 'Hello'. But that's not how it works! It prints "Hello" first, then waits 3 seconds, and finally prints the value of i.
What setTimeout() Actually Does
- Closure: The anonymous function
function () { console.log(i); }passed tosetTimeoutforms a closure. This means it "remembers" the variableifrom its surrounding scope (function x). Crucially, it remembers the reference toi, not its value at that exact moment. - Scheduling:
setTimeout()takes this callback function, hands it off to the browser's timer mechanism (or equivalent in other environments), and sets the 3000ms timer. - Non-Blocking: JavaScript execution doesn't pause. It immediately moves to the next line:
console.log('Hello');. - Callback Execution: After 3000ms elapse, the timer mechanism places the callback function onto the message queue. When the JavaScript call stack is empty, the event loop picks up the callback from the queue and executes it. At this point, the callback accesses the current value of the
iit closed over (which is still 99).
Now, let's tackle a common interview question that combines setTimeout and closures within a loop.
The Challenge: Sequential Logging with Delay
Problem: Print the numbers 1, 2, 3, 4, 5 to the console, but with a delay. Print 1 after 1 second, 2 after 2 seconds, 3 after 3 seconds, and so on. How would you achieve this?
A common first attempt involves using a for loop with setTimeout() inside, like this:
| index.js | |
|---|---|
Letβs check the output:
This is not what we wanted! Why does it print 6 five times? π§
Explanation:
- Closures Capture References: As we established, the callback function passed to
setTimeoutforms a closure. When usingvar, this closure captures a reference to the same variableiacross all loop iterations.varhas function scope (or global scope), not block scope, so there's only oneivariable being updated. - Loops Finish Quickly: JavaScript doesn't wait for
setTimeouttimers. Theforloop runs to completion almost instantly. It schedules fivesetTimeoutcallbacks (for 1000ms, 2000ms, ..., 5000ms). - Final Value of
i: By the time the loop finishes, the conditioni <= 5becomes false. This happens wheniis incremented to 6. So, the singleivariable now holds the value 6. - Callback Execution Time: Later, when the first timer (1000ms) expires and its callback function finally runs, it looks up the value of the
iit has a reference to. At this point,iis 6. The same happens for the callbacks at 2000ms, 3000ms, etc. All five callbacks reference the samei, which holds the value 6.
π€― Why Do They Point to the Same Reference?
Because var declarations are scoped to the nearest function (or globally), not to the loop block ({...}). In the y() function, there is only one i variable created. Each iteration of the loop updates this single variable. All the setTimeout callbacks created within the loop close over this same i.
π‘ How Can We Fix This?
We need each setTimeout callback to close over the value of i as it was during that specific loop iteration.
Solution 1: Use let (Modern JavaScript)
The simplest and most common solution today is to use let instead of var:
| index.js | |
|---|---|
It works!
β¨ Why let Works
- Block Scope:
let(andconst) have block scope. This means a newivariable is created for each iteration of theforloop block. - Separate Closures: When the
setTimeoutcallback is created in each iteration, it forms a closure over that iteration's specific, uniqueivariable. - Correct Values Captured: The first callback closes over an
iwith value 1, the second closes over a differentiwith value 2, and so on. When the callbacks eventually execute, they each access the correct value they captured.
Solution 2: Use var with Closures (Older Approach)
What if you must use var (perhaps due to older environment constraints or interview requirements)? You need to manually create a new scope for each iteration.
Method A: Using a separate function
| index.js | |
|---|---|
Method B: Using an Immediately Invoked Function Expression (IIFE)
| index.js | |
|---|---|
Both a() and b() produce the desired output:
Why these var solutions work:
- New Scope Creation: Both the separate function (
createClosure) and the IIFE create a new function scope during each loop iteration. - Passing by Value: The loop's
iis passed as an argument (value) into this new scope. Inside the scope,valueholds the value ofiat that specific moment in the iteration (1, then 2, then 3...). - Closure Captures Local Value: The
setTimeoutcallback is created inside this new scope. It forms a closure over thevaluevariable, which is local to that scope and holds the correct number for that iteration.
For more examples and explanations of setTimeout, you might find this article helpful:
Playing with JavaScript setTimeout