1

Anyone has any insight on why the following code works? Notice it's still using var in loop rather than let.

for( var i = 0; i < 5; i++) {
  new Promise((resolve, reject) => {
    console.log(i);
    resolve();
  });
}
// Output: 0,1,2,3,4

We know that if setTimout is used here, it queues up 5 macrotasks and will reference i in closure after loop is done, printing out all 5's eventually.

But if promise is being used here, why the microtask queue can accurately scope the var i value?

Any reference would be appreciated.

4 Answers4

2

Though I was unable to find any direct citation explaining this it has been my experience that the executor function to a Promise constructor is executed synchronously.

Though it provides the resolve and reject functions as callbacks to manage any asynchronous code being evaluated or queued within the executor function the executor function itself is not only synchronous but also executed synchronously in the greater context of the Promise object being instantiated.

That would explain why the console.log will log in order within that loop but using a setTimeout would be performed deferred after the variable i has since moved on to its final value.

Sukima
  • 9,965
  • 3
  • 46
  • 60
2

That happens because that code never gets executed in any microtask. The promise executor is called inside the Promise constructor function, only the then callbacks are called inside a microtask.

You can see this if you run this code:

new Promise((res, rej) => {
  console.log("Hi from the promise executor!");
  res();
}).then(() => {
  console.log("Hi from the `then` callback (from a microtask)");
});
console.log("Stack emptied");

So your code is equivalent to this:

for (var i = 0; i < 5; i++) {
  ((resolve, reject) => {
    console.log(i);
    // resolve();
  })();
}
D. Pardal
  • 6,173
  • 1
  • 17
  • 37
0

To start with, Promise callback argument is invoked synchronously, so there's no surprise in there, but regarding this statement:

We know that if setTimout is used here, it queues up 5 macrotasks and will reference i in closure after loop is done, printing out all 5's eventually.

This is wrong.

for(var i = 0; i < 5; i++)
  setTimeout(console.log, 0, i);

You can schedule with both setTimeout and setInterval not just callbacks, but the value these callbacks will receive as parameters.

That means that the for loop in there will log 0, 1, 2, 3, and 4, in this exact order.

Andrea Giammarchi
  • 3,038
  • 15
  • 25
  • 1
    I think the OP meant `setTimeout(() => { console.log(i); });` – D. Pardal Jul 16 '20 at 17:49
  • possible, but that's just the wrong choice when you want to forward values to the callback you are scheduling, and it's a very common mistake/misconception of `setTimeout` and `setInterval` usage. – Andrea Giammarchi Jul 16 '20 at 17:50
0

For a fair comparison, you would want to place the console.log within the resolve like this

for( var i = 0; i < 5; i++) {
  new Promise((resolve, reject) => {
    resolve(console.log(i));
  });
}

Still, this would yield the same results because there is no async delay. But then why does a setTimeout that has 0 delay not produce the same result?

NodeJS has a first in last out callstack, with each cycle referred to as a tick. When a setTimeout resolves, it gets placed in the message queue, which doesn't run until the next tick. When a promise resolves, it gets to cut back in line immediately within the current tick.

The call stacks therefor look like

setTimeout:

timeout i ++ > timeout > i ++ > timeout > i ++ > timeout > i ++ > timeout > i ++ > log > log > log > log > log 

promise:

promise > resolve > log > i++ > promise > resolve > log > i++ > promise > resolve > log > i++ > promise > resolve > log > i++ > promise > resolve > log > i++

So you can see that by the time the console.logs within setTimeout are called, i has already been equated to 5, where as the promise console.logs get to cut back in line before i is iterated.

for( var i = 0; i < 5; i++) {
  setTimeout( ()=> {
    console.log("setTimeout:",i);
  });
}

for( var i = 0; i < 5; i++) {
  new Promise((resolve, reject) => {
    resolve(console.log("Promise:",i));
  });
}
Also notice that the Promise console.logs occur before setTimeout console.logs even though the setTimeout loop occurs first. This shows the current tick includes both for loops, so any setTimeout console.log has to wait until they both complete.
Pavlos Karalis
  • 2,893
  • 1
  • 6
  • 14