3

I have been playing with Promises, but I am having trouble understanding what is happening with the following code:

const promise = new Promise((resolve, reject) => {
  console.log('Promise started - Async code started')
  setTimeout(() => {
    resolve('Success')
  }, 10)
})

setTimeout(() => {
  console.log('Promise log inside first setTimeout')
}, 0)

promise.then(res => {
  console.log('Promise log after fulfilled')
})

console.log('Promise made - Sync code terminated')

setTimeout(() => {
  console.log('Promise log inside second setTimeout')
}, 0)

The output is:


Promise started - Async code started 
Promise made - Sync code terminated 
Promise log inside first setTimeout 
Promise log inside second setTimeout 
Promise log after fulfilled

It is as expected.

But let check the output of the below code:

const promise = new Promise((resolve, reject) => {
  console.log('Promise started - Async code started')
  setTimeout(() => {
    resolve('Success')
  }, 1)
})

setTimeout(() => {
  console.log('Promise log inside first setTimeout')
}, 0)

promise.then(res => {
  console.log('Promise log after fulfilled')
})

console.log('Promise made - Sync code terminated')
setTimeout(() => {
  console.log('Promise log inside second setTimeout')
}, 0)

Changed the to be resolved promise setTimeout timer value from 10ms to 1ms

The output is:

Promise started - Async code started 
Promise made - Sync code terminated 
Promise log after fulfilled 
Promise log inside first setTimeout 
Promise log inside second setTimeout 

Any explanation for this?

Pavan Gangireddy
  • 417
  • 1
  • 3
  • 13
  • [Timeouts are throttled](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified). Different browsers (JavaScript engines) behave differently in this scenario. It looks like Chrome does not differentiate between very small delays and `0`. – str May 05 '20 at 08:55
  • Possible duplicate of [What is minimum millisecond value of setTimeout?](https://stackoverflow.com/q/9647215/1048572). The `1` is treated as if it was `0`, in which case the logs starts to make sense again. – Bergi May 05 '20 at 10:19
  • @Bergi it's actaully `0` which is treated as `1` in Chrome. – Kaiido Jul 10 '20 at 04:31

3 Answers3

3

From Concurrency model and the event loop

  • setTimeout does not run immediately after its timer expires
  • Zero delay doesn't actually mean the call back will fire-off after zero milliseconds. Calling setTimeout with a delay of 0 (zero) milliseconds doesn't execute the callback function after the given interval. Basically, the setTimeout needs to wait for all the code for queued messages to complete even though you specified a particular time limit for your setTimeout.

What happen if we set 2 and 1 milliseconds:

const promise = new Promise((resolve, reject) => {
  console.log('Promise started - Async code started')
  setTimeout(() => {
    resolve('Success')
  }, 2)
})

console.log('Promise log inside first setTimeout 1')
setTimeout(() => {
  console.log('Promise log inside first setTimeout 2')
}, 1)

promise.then(res => {
  console.log('Promise log after fulfilled ❌')

})

console.log('Promise log inside second setTimeout 1')
setTimeout(() => {
  console.log('Promise log inside second setTimeout 2')
}, 1)
});

The output always will be:

Promise started - Async code started
Promise log inside first setTimeout 1
Promise log inside second setTimeout 1
Promise log inside first setTimeout 2
Promise log inside second setTimeout 2
Promise log after fulfilled ❌

Conclusion

If you want a proper behavior, worth to get rid of Zero delays.

Maxim Shoustin
  • 77,483
  • 27
  • 203
  • 225
  • Still that doesn't explain why 1ms timeout are scheduled to fire before the 0ms, even if what happens before the callbacks are executed takes ages, they should have been scheduled with 0ms first and 1ms after, just like your 1 and 2 example, which may very well not fire either at 1 and 2ms after they've been scheduled. https://jsfiddle.net/63km472d/ – Kaiido Jul 10 '20 at 04:54
1

I will use the following example to explain:

setTimeout(() => {
  console.log('1 ms timeout');
}, 1);                            // Moved to async queue at time = T0
setTimeout(() => {
  console.log('0 ms timeout')
}, 0);                            // Moved to async queue after 1 ms that synchronous call to setTimeout takes i.e. at T1
                                  // So at T1, queue will be [("1ms timeout", 0), ("0ms timeout", 0)]

Hence this will print

1 ms timeout
0 ms timeout

Understanding of above: Calling setTimeouts is synchronous (even though its callback is put in async queue), i.e. we call setTimeout() and move to next statement - this synchronous action itself may take 1ms.

In other words, 1ms is too low a time so by the time JS engine sees the 2nd async statement, the first one has already spent 1ms in the queue.

I also suggest you try out the following

setTimeout(() => {
  console.log("First");
}, 2);                      // queue at T0 = [("First", 2)]

const forLoopLimit = 100;
for (var i = 0; i < forLoopLimit; i++){
    console.log(i * 10000);
}                           // Assume that it takes about 3 milliseconds
                            // queue at T3 = [("First", 0)]
setTimeout(() => {
  console.log("Second");
}, 0);                      // Assume it takes 0 milliseconds.
                            // queue at T4 = [("First", 0), ("Second", 0)]

This will print First before Second even though the former had 2ms timeout compared to the latter having 0ms. Now change forLoopLimit to 1 or even 10, you'll see that the synchronous task doesn't take 3 milliseconds now, and Second is printed before First

Also worth trying is:

console.log(Date.now());
console.log(Date.now());

Try above multiple times and you'll see that sometimes console logs will have different timestamps. Roughly, you can say console.log() and Date.now() take 0.5ms. It's nothing but the time to call / execute synchronous stuff.

chiragrtr
  • 902
  • 4
  • 6
  • 1ms to call setTimeout? Seriously? Nhaa, you may be on a very lucky day and you called the first one at T00:00:00.00999... and the second one falls on T:00:00:00.0100 but that's about it and wouldn't be a deterministic behavior but random. – Kaiido Jul 10 '20 at 05:04
0

Chrome has an hardcoded minimum timeout of 1ms.

  base::TimeDelta interval_milliseconds =
      std::max(base::TimeDelta::FromMilliseconds(1), interval);

So for Chrome, all your setTimeout( fn , 0 ) are converted to setTimeout( fn , 1 ), and thus scheduled to fire after the first one you did schedule (remember Promise constructors are called synchronously).

So we could actually simplify your example with

setTimeout( () => console.log( '1ms delay' ), 1 );
setTimeout( () => console.log( '0ms delay' ), 0 );

and in Chrome, the 1ms delay will always fire first, contrary to the common sense because internally it will actually be:

setTimeout( () => console.log( '1ms delay' ), Math.max(1, 1) );
setTimeout( () => console.log( '0ms delay' ), Math.max(1, 0) );

If you had it set to 1 and 2 instead of 0 and 1, your expectation would be fulfilled.

const promise = new Promise((resolve, reject) => {
  console.log('Promise started - Async code started')
  setTimeout(() => {
    resolve('Success')
  }, 2)
})

setTimeout(() => {
  console.log('Promise log inside first setTimeout')
}, 1)

promise.then(res => {
  console.log('Promise log after fulfilled')
})

console.log('Promise made - Sync code terminated')
setTimeout(() => {
  console.log('Promise log inside second setTimeout')
}, 1)
Kaiido
  • 123,334
  • 13
  • 219
  • 285