kjunz
kjunz

Reputation: 111

Process.nextTick and Promise callback

when I run

    process.nextTick(() => console.log(8)) 
    Promise.resolve("hi").then(t => console.log(t)) 
    console.log(7); 
    
    Output is 7 8 hi

The output is expected because process.nextTick runs after the stack is empty and before we start processing the task queue. But when I run

function main() {

  console.log(1);
  console.log(2);
  func();
  console.log(6);
}

function func() {
  console.log(4);
  console.log(5);
  Promise.resolve() // callback gets pushed to microtask queue
  .then(() => { // this callback is in callstack now after logging 6
      process.nextTick(() => console.log(8)) 
      Promise.resolve("hi").then(t => console.log(t)) 
      console.log(7); 
  })
}
Output is 1 2 4 5 6 7 hi 8

Why does the last part log 7 hi 8 instead of 7 8 hi?

Upvotes: 2

Views: 882

Answers (2)

Yousaf
Yousaf

Reputation: 29282

Callback function passed to the process.nextTick is invoked asynchronously and is executed after the current operation has finished.

In your case, the current operation is the processing of the micro-task queue. Once the processing of the micro-task queue is started, it continues until all the micro-tasks in the queue have been processed.

Any micro-tasks enqueued by the existing micro-tasks in the micro-task queue are also processed, which can lead to infinite loop if you are not careful.

In the following code

Promise.resolve() 
 .then(() => { 
     process.nextTick(() => console.log(8)) 
     Promise.resolve("hi").then(t => console.log(t)) 
     console.log(7); 
 })

after scheduling the callback passed to process.nextTick, current micro-task (callback function of then()) queues another micro-task:

Promise.resolve("hi").then(t => console.log(t)) 

This micro-task will also be processed in the current phase of processing the micro-task queue.

Once the micro-tasks have been processed, event loop will process the callback function passed to the process.nextTick before moving to the next phase of processing different callbacks in different queues.

For further details, see: The Node.js Event Loop, Timers, and process.nextTick()

Upvotes: 1

Deepal
Deepal

Reputation: 1814

This happens because you are adding promise callbacks (microtasks) recursively.

Promise callbacks are microtasks, and they are added to the microtasks queue managed by v8.

When v8 executes the microtasks queue it will find your first resolved promise callback as the only microtask in the queue. But when v8 executes the callback, another microtask is immediately added to the queue by Promise.resolve("hi").then(t => console.log(t)). Since v8 is still processing microtasks, it will process the second promise resolve callback before moving on.

This will become quite clear if you schedule another microtask inside your second Promise.resolve callback as follows:

function main() {

  console.log(1);
  console.log(2);
  func();
  console.log(6);
}

function func() {
  console.log(4);
  console.log(5);
  Promise.resolve() // callback gets pushed to microtask queue
  .then(() => { // this callback is in callstack now after logging 6
      process.nextTick(() => console.log(8)) 
      Promise.resolve("hi").then(t => {
        console.log(t)
        Promise.resolve("there").then(t => console.log(t))
      }) 
      console.log(7); 
  })
}

The above code will yield the following output:

1
2
4
5
6
7
hi
there
8

I wrote the article series linked by @jfriend00 (Thanks for linking the article). However, this behaviour you observed is consistent across all NodeJS versions. (at least currently stable Node versions at the time of this writing.)

Scheduling microtasks recursively is risky, and it's documented in MDN.

Warning: Since microtasks can themselves enqueue more microtasks, and the event loop continues processing microtasks until the queue is empty, there's a real risk of getting the event loop endlessly processing microtasks. Be cautious with how you go about recursively adding microtasks.

Upvotes: 2

Related Questions