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
- Event Loop and the Big Picture
- Timers, Immediates and Next Ticks
- Promises, Next-Ticks, and Immediates (This article)
- Handling I/O
- Event Loop Best Practices
- New changes to timers and microtasks in Node v11
Native Promises
There are some changes introduced in Node v11 which significantly changes the execution order of
nextTick
,Promise
callbacks,setImmediate
andsetTimeout
callbacks since Node v11. Read more:
New Changes to the Timers and Microtasks in Node v11.0.0 ( and above) | by Deepal Jayasekara | Deepal’s Blog
Deepal Jayasekara ・ ・
Medium
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.
- 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)
- Two handlers will be added to the
setImmediate
queue. - Three items will be added to the
process.nextTick
queue. - One timer is created with expiration time as zero, which will be immediately expired and the callback is added to the timers queue
- Two items will be added again to the
setImmediate
queue.
Then the event loop will start checking the process.nextTick
queue.
- 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. - 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.
- During the process of promises microtask queue, one item is again added to the
process.nextTick
queue (‘next tick inside promise resolve handler’). - 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. - 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.
- 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. - 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 thatthen
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 promisereject
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');
BlueBird.setScheduler(process.nextTick);
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?
- 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
? - 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!
References
- Bluebird Docs http://bluebirdjs.com/docs/
- Bluebird Git Repo https://github.com/petkaantonov/bluebird
- Q Git Repo https://github.com/kriskowal/q
Background Image Courtesy: https://wallpapersite.com/images/wallpapers/the-flash-5120x2880-grant-gustin-season-3-hd-7576.jpg
Top comments (0)