3

I have an unsolved JS behavior that I cannot understand. I'm running this code on node v8.4.0. I'm running this code twice.

First time with f1() second time with f2()

f2() the result is as expected. 'start' is printed first and then 'end'.

f1() the result is not as expected. 'end' is printed first and then 'start'.

Can someone please explain to me the result of the code below?

const fs = require('fs')

function f1() { return new Promise((resolve, reject) => { resolve() }) }

function f2() {
    return new Promise((resolve, reject) => {
        fs.readFile('/Users/adi/Downloads/profile.jpg', resolve)
    })
}

async function main() {

    setImmediate(() => { console.log('start') })
    await f1()
    console.log('end')
} 

main()

//f1 output:
end
start

//f2 output:
start
end

As far as I know, the result should be 'start' and then 'end'. What am I missing?

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
Adi Azarya
  • 4,015
  • 3
  • 18
  • 26
  • i wonder if the `Promise` constructor is simplifying the result. – Daniel A. White Aug 19 '18 at 13:51
  • 1
    So, what you're really asking is about a race between `setImmediate()` and the `.then()` handler of an immediately resolved promise? The answer is because of the internals of how different async things like `setImmediate()` and promises are coded to work (micro tasks). If you care about the order between the two, then write your code for a specific order rather than rely on that level of internal design detail. – jfriend00 Aug 19 '18 at 16:39
  • 1
    `setImmediate` is _very_ unfortunately named, it does _anything_ but run the code immediately - it actually runs it _after_ all microtasks (promises and await) and _after_ `nextTick` (which does _not_ run on the next tick but rather is a microtask) – Benjamin Gruenbaum Aug 19 '18 at 17:38

3 Answers3

1

The queue with the resolved Promises will be check before the queue with setImmediate(() => { console.log('start') })

Because the f1 resolves immediately, both the callback of the setImmediate and the resolved Promise are added to the event queue at the same time, but at different stages. Resolved Promises have a higher priority then callbacks added with setImmediate

If you use process.nextTick then the callback will be added with an higher priority then setImmediate and start will be logged before end

function f1() { return new Promise((resolve, reject) => { resolve() }) }

async function main() {
    process.nextTick(() => { console.log('start') })
    setImmediate(() => { console.log('start') })
    await f1()
    console.log('end')
} 

main()

For f2 the reading of the file will involve a longer lasting async task so the setImmediat will be still called before.

t.niese
  • 39,256
  • 9
  • 74
  • 101
  • 2
    More info https://jsblog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa and https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ – ben Aug 19 '18 at 14:19
1

So, in your f1() example, you have a race between setImmediate() and the .then() handler of an immediately resolved promise since both will be in the event queue at the time the next event is ready to be processed.

When both are ready to run, one runs before the other because of the internals of how different async things like setImmediate() and promises are coded to work in the node.js implementation of its event loop. Internal to the event loop in node.js, there is a sequence or priority for some different types of asynchronous operations and some go before others if all are waiting to go. It is possible, though difficult, to fully understand which goes before the others, but it is very complicated and it is mostly an implementation detail, not something fully documented by specification.

In this specific case, native promises in node.js use a microTasks queue (there are apparently a couple separate microTasks queues) and they are run before things like setImmediate(), timers and I/O events.

But, in general, it is best to not rely on fully understanding all that and, if you want one thing to happen before the other, don't allow it to be a race between the two inside of node.js. Just code it with your own code to force the sequence you want. This also makes your code more obvious and declarative what order you expect things to be processed in.

If I read your current code, I would think that you purposely set up a race between f1() and setImmediate() and did not care which one ran first because the code is not declarative and does not define a desired order.

For more info on the details of the internals of different types of async operations in the event loop, you can read these references:

Promise.resolve().then vs setImmediate vs nextTick

Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3

Promises wiggle their way between nextTick and setImmediate

Here's a quote from this last reference article:

Native promise handlers are executed on a microtask queue which is roughly the same as nextTick, so they run before everything else. Pure javascript [promise] implementations should use nextTick for scheduling.


For your f2() example, it's probably just that fs.readFile() takes some finite amount of time so f2() does not resolve immediately and thus isn't ready to run at the same time that setImmediate() is so the setImmediate() runs before f2() resolves.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • We actually absolutely guarantee that timers run after promises. There is actually one buggy edge case where it won't happen now - but in general it's absolutely the intention of the Node.js project as well as browser behaviour. You still shouldn't rely on it. – Benjamin Gruenbaum Aug 19 '18 at 17:39
  • Also, I'm not sure it's true that promises "use the microtask queue", there are actually two microtask queues: Node's and V8's. Promises use the V8 microtask queue (RunMicrotasks) whereas `nextTick` queued tasks are served internally. – Benjamin Gruenbaum Aug 19 '18 at 17:41
0

It's works like this, because Promise is a microtask. Microtasks are executed at the end of call stack, before macrotasks. You can read more here

Krzysztof Grzybek
  • 8,818
  • 2
  • 31
  • 35