javascriptsettimeoutevent-loop

Why setTimeout is executing this way


Code:

setTimeout(() => console.log("1"), 1000)

const startTime = Date.now()
while (Date.now() - startTime < 5000);

setTimeout(() => console.log("2"), 0)
setTimeout(() => console.log("3"), 0)

The output of this sample is:

2
3
1

Why it works this way ?
In this example the loop blocks the thread for 5 seconds or so. At this moment first setTimeout should have been finished and go out of the macrotask queue first, but it doesn't.


Solution

  • This appears to be a side effect of setTimeout optimisation in Chromium, but I can't find documentation that specifically would make this a bug. Quite the opposite, setTimeout does not guarantee execution order.

    If you change the timeouts to 1ms it works as you expect:

    setTimeout(() => console.log("1"), 1000)
    
    const startTime = Date.now()
    while (Date.now() - startTime < 5000);
    
    setTimeout(() => console.log("2"), 1)
    setTimeout(() => console.log("3"), 1)

    It looks like, in Chromium at least, setTimeout has an optimisation on 0ms to skip checking for timed out actions, and instead just runs them on the next loop (like requestAnimationFrame would) before checking for any other expired timeouts.

    Arguably this is desirable behaviour when using a 0ms timeout, and I don't think it's a bug because there's no guarantees on the timeouts - that's in the spec.

    I'd recommend using requestAnimationFrame over setTimeout(..., 0) for consistency.


    Original answer....

    Your problem is that this:

    for (let i = 0; i < 10000; i++) {
      for (let j = 0; j < 10000; j++) {}
    }
    

    Is not a slow operation. Most JS engines can optimise loops and additions like this.

    You can use await to stall your execution and come back:

    /** Wait for the given millseconds */
    function wait(ms) {
      return new Promise(r => setTimeout(r, ms));
    }
    
    async function demo() {
      setTimeout(() => console.log('1'), 1000)
    
      await wait(5000);
    
      setTimeout(() => console.log('2'), 0)
      setTimeout(() => console.log('3'), 0)
    }
    
    demo();

    But, that doesn't block the JS - you'll get 1 after 1000ms and then wait 4 more seconds for 2 and 3.

    If you want to block the executing JS that's harder, as they've worked very hard to remove blocking actions. For instance fetch doesn't support it, and while XMLHttpRequest has an async=false flag you could use to block while downloading something large, I think some browsers ignore that now.