8

Let's imagine we have a file containing next JS code:

process.nextTick(()=>{
    console.log('nextTick')
})

queueMicrotask(()=>{
    console.log('queueMicrotask')
})

console.log('console.log')

and we've set module system to "type": "commonjs" in our package.json

What output in console do we expect? Node.js official documentation says:

The process.nextTick() queue is always processed before the microtask queue within each turn of the Node.js event loop

So, in console we expect

console.log
nextTick
queueMicrotask

And that works. Until I change module system to "type": "module". After that change I constantly have queueMicrotask executed before process.nextTick . Console output is:

console.log
queueMicrotask
nextTick

Can anybody explain this behaviour? I assume that this behaviour is somehow related to module evaluation and execution process and that nextTick queueMicrotask somehow get into different event loop ticks as module implies async execution(in browser). Still, that guess is very shaky and from my point of view, is illogical. Nevertheless, I still don't have a plausible explanation.

Jan Molak
  • 4,426
  • 2
  • 36
  • 32
  • 2
    Didn't check if that's the case or not, but it wouldn't be surprising that they do execute ES modules from a microtask checkpoint directly (moreover since they do support the top-level-await). In that case the next microtask will get executed right after, since the queue gets refilled directly. – Kaiido Dec 29 '21 at 12:28
  • @Kaiido Sounds reasonable, because if I extend code like below `process.nextTick(()=>{ console.log('nextTick') }) queueMicrotask(()=>{ console.log('queueMicrotask') }) setImmediate(()=>{ queueMicrotask(()=>{ console.log('queueMicrotask 2') }) process.nextTick(()=>{ console.log('nextTick 2') }) }) console.log('console.log')` nextTick 2 and queueMicrotask 2 appear in right order ` console.log queueMicrotask nextTick nextTick 2 queueMicrotask 2 ` Would much appreciate if somebody can share ref to datasheet of that – Artyom Kravchenko Dec 29 '21 at 13:08

1 Answers1

2

This is because, when ran as ESM, the script is actually already in the microtask phase. So a new microtask queued from there will get executed before a "nextTick" callback.


Both queues behave the same here in that they're being emptied "live" until completion, and thus any new queued callback will get executed before any other queue is visited.

queueMicrotask(() => {
  let winner;
  console.log("in microtask, the winner is");
  queueMicrotask(() => {
    if (!winner) console.log(winner = "microtask");
  });
  process.nextTick(() => {
    if (!winner) console.log(winner = "nextTick");
  });
});
process.nextTick(() => {
  let winner;
  console.log("in nextTick, the winner is");
  queueMicrotask(() => {
    if (!winner) console.log(winner = "microtask");
  });
  process.nextTick(() => {
    if (!winner) console.log(winner = "nextTick");
  });
});

The above script will always output "In microtask the winner is microtask" and "In nextTick the winner is nextTick", no matter how the main script is evaluated. (The order of both statements will change though).


In node, ESM module scripts are actually evaluated from an async/await function:

 async run() {
  await this.instantiate();
  const timeout = -1;
  const breakOnSigint = false;
  try {
    await this.module.evaluate(timeout, breakOnSigint);

So this means that when our script is ran, we're already inside the microtask phase, and thus new microtasks that get queued from there will get executed first.

When ran as CommonJS though, we're still in the poll phase. The entry point is here, then everything until the actual evaluation is synchronous. And from the poll phase, nextTick will win against the microtask phase:

setImmediate(() => {
  let winner;
  console.log("in poll phase, the winner is");
  queueMicrotask(() => {
    if (!winner) console.log(winner = "microtask");
  });
  process.nextTick(() => {
    if (!winner) console.log(winner = "nextTick");
  });
});

This will always output "in poll phase the winner is nextTick", no matter how the main script is being evaluated.

Kaiido
  • 123,334
  • 13
  • 219
  • 285