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 tosetTimeout
forms a closure. This means it "remembers" the variablei
from 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
i
it 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
setTimeout
forms a closure. When usingvar
, this closure captures a reference to the same variablei
across all loop iterations.var
has function scope (or global scope), not block scope, so there's only onei
variable being updated. - Loops Finish Quickly: JavaScript doesn't wait for
setTimeout
timers. Thefor
loop runs to completion almost instantly. It schedules fivesetTimeout
callbacks (for 1000ms, 2000ms, ..., 5000ms). - Final Value of
i
: By the time the loop finishes, the conditioni <= 5
becomes false. This happens wheni
is incremented to 6. So, the singlei
variable 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
i
it has a reference to. At this point,i
is 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 newi
variable is created for each iteration of thefor
loop block. - Separate Closures: When the
setTimeout
callback is created in each iteration, it forms a closure over that iteration's specific, uniquei
variable. - Correct Values Captured: The first callback closes over an
i
with value 1, the second closes over a differenti
with 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
i
is passed as an argument (value
) into this new scope. Inside the scope,value
holds the value ofi
at that specific moment in the iteration (1, then 2, then 3...). - Closure Captures Local Value: The
setTimeout
callback is created inside this new scope. It forms a closure over thevalue
variable, 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