Understanding unexpected Behaviours of setTimeout
Consider the following code:
console.log("start");
setTimeout(() => {
console.log("callback");
}, 5000);
console.log("end");
In this example, the setTimeout
function is used to schedule a callback after 5 seconds. You might have seen a similar diagram in Event Loop blogs where we explain how the Event Loop handles setTimeout
. Here's a breakdown of what happens:
Event Loop and setTimeout
Initial Execution:
The first
console.log("start")
is immediately executed and logged to the console.The
setTimeout
function is encountered:It registers its callback inside the Web APIs and sets a timer for 5 seconds.
The
setTimeout
function itself is removed from the Call Stack after registration.
console.log("end")
Execution:The
console.log("end")
statement is executed immediately after thesetTimeout
registration, logging"end"
to the console.Once this is done, it is also removed from the Call Stack.
Timer Expiration:
After 5 seconds, the callback is moved to the Callback Queue.
However, the callback will not execute until the Call Stack is empty and the Event Loop finds it.
What Happens When the Main Thread is Blocked?
Now, consider a scenario where the main thread is blocked and is executing a task for a much longer duration (e.g., 10 seconds). In this case, the setTimeout
callback that was scheduled 5 seconds ago is still waiting in the Callback Queue.
Even though the timer has expired, the Event Loop cannot move the callback from the Callback Queue to the Call Stack until the Call Stack is empty.
So, after the main thread finishes
its 10-second task and the Call Stack becomes empty, the Event Loop will finally execute the setTimeout
callback from the Callback Queue.
This scenario illustrates concurrency in JavaScript—specifically, how asynchronous code like setTimeout
is handled in a single-threaded environment. Even if the callback has been scheduled for a long time, it will only be executed when the Call Stack is clear, highlighting that JavaScript is non-blocking, but still single-threaded.
Blocking the Main Thread: Observing setTimeout
Behavior
Let’s explore what happens when the main thread is blocked for 10 seconds:
console.log("start");
setTimeout(() => {
console.log("callback");
}, 5000);
console.log("end");
let startDate = new Date().getTime();
let endDate = startDate;
while (endDate < startDate + 10000) {
endDate = new Date().getTime();
}
console.log("while expires");
Explanation
Initial Execution:
console.log("start")
is executed and logs"start"
.setTimeout
is encountered, and its callback is registered in the Web APIs with a 5-second timer.console.log("end")
is executed and logs"end"
.
Blocking the Main Thread:
The
while
loop simulates a blocking operation by keeping the main thread busy for 10 seconds.During this time, the Call Stack remains occupied, preventing the Event Loop from processing any tasks in the Callback Queue.
Timer Expiration:
Although the 5-second timer for
setTimeout
expires during thewhile
loop, its callback remains in the Callback Queue because the Call Stack is still busy.Once the
while
loop completes,"while expires"
is logged, and the Call Stack is cleared.
Callback Execution:
- The Event Loop now picks the
setTimeout
callback from the Callback Queue and executes it, logging"callback"
.
- The Event Loop now picks the
Case with setTimeout
Set to 0
Consider another example where the setTimeout
duration is set to 0
:
console.log("start");
setTimeout(() => {
console.log("callback");
}, 0);
console.log("end");
let startDate = new Date().getTime();
let endDate = startDate;
while (endDate < startDate + 10000) {
endDate = new Date().getTime();
}
console.log("while expires");
In this case, the
setTimeout
callback is still registered in the Web APIs, but the0
milliseconds duration doesn’t mean it executes immediately.The callback is moved to the Callback Queue right after registration, but like before, it must wait for the Call Stack to clear.
Since the Call Stack is blocked by the
while
loop for 10 seconds, the callback will only execute once the loop finishes and the thread is free.
Key Insights
setTimeout
Doesn't Skip the Queue: Even withsetTimeout(0)
, the callback is queued and will wait until the Call Stack is empty.Blocking the Main Thread Delays Execution: If the Call Stack is blocked (e.g., by a heavy computation or synchronous operation like the
while
loop), thesetTimeout
callback will be delayed until the thread is free.`