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

  1. 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.

  2. console.log("end") Execution:

    • The console.log("end") statement is executed immediately after the setTimeout registration, logging "end" to the console.

    • Once this is done, it is also removed from the Call Stack.

  3. 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

  1. 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".

  2. 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.

  3. Timer Expiration:

    • Although the 5-second timer for setTimeout expires during the while 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.

  4. Callback Execution:

    • The Event Loop now picks the setTimeout callback from the Callback Queue and executes it, logging "callback".

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");
  1. In this case, the setTimeout callback is still registered in the Web APIs, but the 0 milliseconds duration doesn’t mean it executes immediately.

  2. The callback is moved to the Callback Queue right after registration, but like before, it must wait for the Call Stack to clear.

  3. 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

  1. setTimeout Doesn't Skip the Queue: Even with setTimeout(0), the callback is queued and will wait until the Call Stack is empty.

  2. 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), the setTimeout callback will be delayed until the thread is free.`