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

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

deepal profile image Deepal Jayasekara Originally published at blog.insiderattack.net ・6 min read

Welcome back to the Event Loop article series! In the first article of the series, we discussed the overall picture of the Node JS event loop and it’s different stages. Later in the second article, we discussed what timers and immediates are in the context of the event loop and how each queue is scheduled. In this article, let’s look at how event loop schedules resolved/rejected promises (including native JS promises, Q promises, and Bluebird promises) and next tick callbacks. If you are not familiar with Promises yet, I suggest you should first get in touch with Promises. Believe me, it’s so cool!!

Post Series Roadmap

Native Promises

There are some changes introduced in Node v11 which significantly changes the execution order of nextTick, Promise callbacks, setImmediate and setTimeout callbacks since Node v11. Read more:

In the context of native promises, a promise callback is considered as a microtask and queued in a microtask queue which will be processed right after the next tick queue.

Consider the following example.

In the above example, the following actions will happen.

  1. Five handlers will be added to the resolved promises microtask queue. (Note that I add 5 resolve handlers to 5 promises which are already resolved)
  2. Two handlers will be added to the setImmediate queue.
  3. Three items will be added to the process.nextTick queue.
  4. One timer is created with expiration time as zero, which will be immediately expired and the callback is added to the timers queue
  5. Two items will be added again to the setImmediate queue.

Then the event loop will start checking the process.nextTick queue.

  1. Loop will identify that there are three items in the process.nextTick queue and Node will start processing the nextTick queue until it is exhausted.
  2. Then the loop will check the promises microtask queue and identify there are five items in the promises microtask queue and will start processing the queue.
  3. During the process of promises microtask queue, one item is again added to the process.nextTickqueue (‘next tick inside promise resolve handler’).
  4. After promises microtask queue is finished, event loop will again detect that there is one item is in the process.nextTick queue which was added during promises microtask processing. Then node will process the remaining 1 item in the nextTick queue.
  5. Enough of promises and nextTicks. There are no more microtasks left. Then the event loop moves to the first phase, which is the timers phase. At this moment it will see there is an expired timer callback in the timers queue and it will process the callback.
  6. Now that there are no more timer callbacks left, loop will wait for I/O. Since we do not have any pending I/O, the loop will then move on to process setImmediate queue. It will see that there are four items in the immediates queue and will process them until the immediate queue is exhausted.
  7. At last, loop is done with everything…Then the program gracefully exits.

Enough of seeing the two words “promises microtask” everywhere instead of just “microtask”?

I know it’s a pain to see it everywhere, but you know that resolved/rejected promises and _process.nextTick_ are both microtasks. Therefore, trust me, I can’t just say nextTick queue and microtask queue.

So let’s see how the output will look like for the above example.

next tick1
next tick2
next tick3
promise1 resolved
promise2 resolved
promise3 resolved
promise4 resolved
promise5 resolved
next tick inside promise resolve handler
set timeout
set immediate1
set immediate2
set immediate3
set immediate4

Q and Bluebird

Cool! We now know that the resolve/reject callbacks of JS native promises will be scheduled as a microtask and will be processed before the loop moves to a new phase. So what about Q and Bluebird?

Before JS native promises were implemented in NodeJS, prehistoric people were using libraries such as Q and Bluebird (Pun intended :P). Since these libraries predate native promises, they have different semantics than the native promises.

At the time of this writing, Q (v1.5.0) uses process.nextTick queue to schedule callbacks for resolved/rejected promises. Based on the Q docs,

Note that resolution of a promise is always asynchronous: that is, the fulfillment or rejection handler will always be called in the next turn of the event loop (i.e. process.nextTick in Node). This gives you a nice guarantee when mentally tracing the flow of your code, namely that then will always return before either handler is executed.

On the other hands, Bluebird, at the time of this writing (v3.5.0) uses setImmediate by default to schedule promise callbacks in recent NodeJS versions (you can see the code here).

To see the picture clear, we’ll have a look at another example.

In the above example, BlueBird.resolve().then callback has the same semantics as the following setImmediate call. Therefore, bluebird’s callback is scheduled in the same immediates queue before the setImmediate callback. Since Q uses process.nextTick to schedule its resolve/reject callbacks, Q.resolve().then is scheduled in the nextTick queue before the succeeding process.nextTick callback. We can conclude our deductions by seeing the actual output of the above program, as follows:

q promise resolved
next tick
native promise resolved
set timeout
bluebird promise resolved
set immediate

Please note that although I have used only promise resolve handlers in the above examples, this behavior is identical for promise reject handlers as well. At the end of this article, I’ll present you an example with both resolve and reject handlers

Bluebird, however, provides us a choice. We can select our own scheduling mechanism. Does it mean we can instruct bluebird to use process.nextTick instead of setImmediate? Yes it does. Bluebird provides an API method named setScheduler which accepts a function which overrides the default setImmediate scheduler.

To use process.nextTick as the scheduler in bluebird you can specify,

constBlueBird = require('bluebird');

and to use setTimeout as the scheduler in bluebird you can use the following code,

constBlueBird = require('bluebird');
BlueBird.setScheduler((fn) => {
    setTimeout(fn, 0);

— To prevent this post from being too long, I’m not going to describe examples of different bluebird schedulers here. You can try out using different schedulers and observe the output yourself —

Using setImmediate instead of process.nextTick has its advantages too in latest node versions. Since NodeJS v0.12 and above does not implement process.maxTickDepth parameter, excessively adding events to the nextTick queue can cause I/O starvation in the event loop. Therefore, it’s safe to use setImmediate instead of process.nextTick in the latest node versions because immediates queue is processed right after I/O if there are no nextTick callbacks and setImmediate will never starve I/O.

One last twist!

If you run the following program you might run into a bit of a mind-twisting output.

q promise resolved
q promise rejected
next tick
native promise resolved
native promise rejected
set timeout
bluebird promise resolved
bluebird promise rejected
set immediate

Now you should have two questions?

  1. If Q uses process.nextTick internally to schedule a resolved/rejected promise callback, how did the log line, q promise rejected come before the line, next tick?
  2. If Bluebird uses setImmediate internally to schedule a resolved/rejected promise callback, how did the line, bluebird promise rejected come before the line, set immediate.

This is because both libraries internally queue resolved/rejected promise callbacks in an internal data structure and use either process.nextTick or setImmediate to process all the callbacks in the data structure at once.

Great! Now that you know a lot about setTimeout, setImmediate, process.nextTick and promises, you should be able to clearly explain a given example of these. If you have any question regarding this article or something to be added, I appreciate if you post them in response. In the next article, I’ll discuss how I/O is processed with the event loop in detail. And believe me, It will be an awesome topic!


Background Image Courtesy: https://wallpapersite.com/images/wallpapers/the-flash-5120x2880-grant-gustin-season-3-hd-7576.jpg


Editor guide