I believe if you are reading this, you must have heard about the famous event loop that Node.js has, how it handles the concurrency mechanism in Node.js and how it makes Node.js a unique platform for event driven I/O. Being an Event driven I/O, all of the code that is executed is in the form of callbacks. Hence, it is important to know how and in what order are all these callbacks executed by the event loop. From here onwards, in this blog post, the term 'event loop' refers to the Node.js' event loop.
The event loop is basically a mechanism which has certain phases through which it iterates. You must also have heard about a term called 'Event Loop iteration' which implies an iteration of event loop over all of its phases.
In this post, I will be going a bit at showing you the lower level architecture of event loop, what all its phases are, which code is executed in which phase, and some specifics and lastly some examples which I think will make you understand better about event loop concepts.
Following is the diagram of what all phases an event loop iterates through as per their order:
So, the event loop is a mechanism in Node.js which iterates over a series of in loop. Following are the phases that the event loop iterates through:
Each of the phases has a queue/heap which is used by the event loop to push/store the callbacks to be executed (There is a misconception in Node.js that there is only a single global queue where the callbacks are queued for execution which is not true.).
Timers:
The callbacks of timers in JavaScript(setTimeout, setInterval) are kept in the heap memory until they are expired. If there are any expired timers in the heap, the event loop takes the callbacks associated with them and starts executing them in the ascending order of their delay until the timers queue is empty. However, the execution of the timer callbacks is controlled by the Poll phase of the event loop (we will see that later in this article).Pending callbacks:
In this phase, the event loop executes system-related callbacks if any. For example, let's say you are writing a node server and the port on which you want to run the process is being used by some other process, node will throw an errorECONNREFUSED
, some of the *nix systems may want the callback to wait for execution due to some other tasks that the operating system is processing. Hence, such callbacks are pushed to the pending callbacks queue for execution.Idle/Prepare: In this phase, the event loop does nothing. It is idle and prepares to go to the next phase.
-
Poll:
This phase is the one which makes Node.js unique. In this phase, the event loop watches out for new async I/O callbacks. Nearly all the callbacks except the setTimeout, setInterval, setImmediate and closing callbacks are executed.
Basically, the event loop does two things in this phase:- If there are already callbacks queued up in the poll phase queue, it will execute them until all the callbacks are drained up from the poll phase callback queue.
- If there are no callbacks in the queue, the event loop will stay in the poll phase for some time. Now, this 'some time' also depends on a few things:
- If there are any callbacks present in the setImmediate queue to be executed, event loop won't stay for a much longer time in the poll phase and will move to the next phase i.e Check/setImmediate. Again, it will start executing the callbacks until the Check/setImmediate phase callback queue is empty.
- The second case when the event loop will move from the poll phase is when it gets to know that there are expired timers, the callback of which are waiting to be executed. In such a case, the event loop will move to the next phase i.e Check/setImmediate and then to the Closing callbacks phase and will eventually start its next iteration from the timers phase.
Check/setImmediate: In this phase, the event loop takes the callbacks from the Check phase's queue and starts executing one by one until the queue is empty. The event loop will come to this phase when there are no callbacks remaining to be executed in the poll phase and when the poll phase becomes idle. Generally, the callbacks of setImmediate are executed in this phase.
Closing callbacks: In this phase, the event loop executes the callbacks associated with the closing events like
socket.on('close', fn)
orprocess.exit()
.
Apart from all these, there is one more microtask
queue which contains callbacks associated with process.nextTick
which we will see in a bit.
Examples
Let us start with a simple example to understand how the following code is executed:
function main() {
setTimeout(() => console.log('1'), 0);
setImmediate(() => console.log('2'));
}
main();
Let us recall the event loop diagram and combine our phase explanation with it and try to figure out the output of the above code:
When executed with node as an interpreter, the output of the above code comes out to be:
1
2
The event loop enters the Timers
phase and executes the callback associated with the setTimeout
above after which it enters the subsequent phases where it doesn't see any callbacks enqueued until it reaches the Check (setImmediate)
phase where it executes the callback function associated with it. Hence the desired output.
Note: The above output can be reversed too i.e
2
1
since the event loop doesn't execute the callback of setTimeout(fn, 0) exactly in 0ms time. It executes the callback after a bit of delay somewhat after 4-20 ms. (Remember?, it was earlier mentioned that the Poll phase controls the execution of the timer callbacks since it waits for some I/O in the poll phase).
Now, there are two things which happen when any JavaScript code is run by the event loop.
- When a function in our JavaScript code is called, the event loop first goes without actually the execution to register the initial callbacks to the respective queues.
- Once they are registered, the event loop enters its phases and starts iterating and executing the callbacks until all them are processed.
One more example or let's say there is a misconception in Node.js that setTimeout(fn, 0) always gets executed before setImmediate, which is not at all true! As we saw in the above example, the event loop was in the Timers phase initially and maybe the setTimeout timer was expired and hence it executed it first and this behaviour is not predictable. However, this is not true always, it all depends on the number of callbacks, what phase the event loop is in, etc.
However, if you do something like this:
function main() {
fs.readFile('./xyz.txt', () => {
setTimeout(() => console.log('1'), 0);
setImmediate(() => console.log('2'));
});
}
main();
The above code will always output:
2
1
Let us see how the above code is executed:
As we call our
main()
function, the event loop first runs without actually executing the callbacks. We encounter the fs.readFile with a callback which is registered and the callback is pushed to the I/O phase queue. Since all the callbacks for the given function are registered, the event loop is now free to start execution of the callbacks. Hence, it traverses through its phases starting from the timers. It doesn't find anything in the Timers and Pending callbacks phase.When the event loop keeps traversing through its phases and when it sees that the file reading operation is complete, it starts executing the callback.
Remember, when the event loop starts executing the callback of fs.readFile
, it is in the I/O phase, after which, it will move to the Check(setImmediate) phase.
- Thus, the Check phase comes before the Timers phase for the current run. Hence, when in I/O phase, the callback of
setImmediate
will always run beforesetTimeout(fn, 0)
.
Let us consider one more example:
function main() {
setTimeout(() => console.log('1'), 50);
process.nextTick(() => console.log('2'));
setImmediate(() => console.log('3'));
process.nextTick(() => console.log('4'));
}
main();
Before we see how the event loop executes this code, there is one thing to understand:
The process.nextTick comes under
microtasks
which are prioritised above all other phases and thus the callback associated with it is executed just after the event loop finishes the current operation. Which means that, whatever callback we pass to process.nextTick, the event loop will complete its current operation and then execute callbacks from themicrotasks
queue until it is drained up. Once the queue is drained up, it returns back to the phase where it left its work from.
- It first checks the
microtask
queue and executes the callbacks in it(process.nextTick callbacks in this case). - It then enters its very first phase (Timers phase) where the 50ms timer is not yet expired. Hence it moves forward to the other phases.
- It then goes to the 'Check (setImmediate)' phase where it sees the timer expired and executes the callback which logs '3'.
- In the next iteration of the event loop, it sees the timer of 50ms expired and hence logs down '1'.
Here is the output of the above code:
2
4
3
1
Consider one more example, this time we are passing an asynchronous callback to one of our process.nextTick
.
function main() {
setTimeout(() => console.log('1'), 50);
process.nextTick(() => console.log('2'));
setImmediate(() => console.log('3'));
process.nextTick(() => setTimeout(() => {
console.log('4');
}, 1000));
}
main();
The output of the above code snippet is:
2
3
1
4
Now, here is what happens when the above code is executed:
- All the callbacks are registered and pushed to their respective queues.
- Since the
microtasks
queue callbacks are executed first as seen in the previous examples, '2' gets logged. Also, at this time, the second process.nextTick callback i.e setTimeout(which will log '4') has started its execution and is ultimately pushed to the 'Timers' phase queue. - Now, the event loop enters its normal phases and executes callbacks. The first phase that it enters is 'Timers'. It sees that the timer of 50ms is not expired and hence moves further to the next phases.
- It then enters 'Check (setImmediate)' phase and executes the callback of setImmediate which ultimately logs '3'.
- Now, the next iteration of the event loop begins. In it, the event loop returns back to the 'Timers' phase, it encounters both the expired timers i.e 50ms and 1000ms as per their registering, and executes the callback associated with it which logs first '1' and then '4'.
Thus, as you saw the various states of event loop, its phases and most importantly, process.nextTick
and how it functions. It basically places the callback provided to it in the microtasks
queue and executes it with priority.
One last example and a detailed one, do you remember the diagram of the event loop at the beginning of this blog post? Well, take a look at the code below. I would like you to figure out what would be the output of the following code. Following the code, I have put a visual of how the event loop will execute the following code. It will help you understand better:
1 const fs = require('fs');
2
3 function main() {
4 setTimeout(() => console.log('1'), 0);
5 setImmediate(() => console.log('2'));
6
7 fs.readFile('./xyz.txt', (err, buff) => {
8 setTimeout(() => {
9 console.log('3');
10 }, 1000);
11
12 process.nextTick(() => {
13 console.log('process.nextTick');
14 });
15
16 setImmediate(() => console.log('4'));
17 });
18
19 setImmediate(() => console.log('5'));
20
21 setTimeout(() => {
22 process.on('exit', (code) => {
23 console.log(`close callback`);
24 });
25 }, 1100);
26 }
27
28 main();
Following gif indicates how does the event loop execute the above code:
Note:
- The numbers in the queues indicated in the following gif are the line number of the callbacks in the above code.
- Since my focus is on how event loop phases execute the code, I haven't inserted the Idle/Prepare phase in the gif since it is used internally only by the event loop.
The above code will output:
1
2
5
process.nextTick
4
3
close callback
OR, it can also be (remember the very first example):
2
5
1
process.nextTick
4
3
close callback
Misc
Microtasks and Macrotasks
- Microtasks
So, there is a thing in Node.js or say v8 to be accurate called 'Microtasks'. Microtasks are not a part of the event loop and they are a part of v8, to be clear. Earlier, in this article, you may have read about process.nextTick
. There are some tasks in JavaScript which come under Microtasks namely process.nextTick
, Promise.resolve
, etc.
These tasks are prioritised over other tasks/phases meaning that the event loop after its current operation will execute all the callbacks of the microtasks
queue until it is drained up after which it resumes its work from the phase it left its work from.
Thus, whenever Node.js encounters any microtask
defined above, it will push the associated callback to the microtask
queue and start the execution right away(microtasks are prioritised) and execute all the callbacks until the queue is drained up thoroughly.
That being said, if you put a lot of callbacks in the microtasks
queue, you may end up starving the event loop since it will never go to any other phase.
- Macrotasks
Tasks such as setTimeout
, setInterval
, setImmediate
, requestAnimationFrame
, I/O
, UI rendering
, or other I/O callbacks
come under the macrotasks. They have no such thing as prioritisation by the event loop. The callbacks are executed according to the event loop phases.
Event loop tick
We say that a 'tick' has happened when the event loop iterates over all of its phases for one time (one iteration of the event loop).
High event loop tick frequency and low tick duration(time spent in one iteration) indicates the healthy event loop.
I hope you enjoyed this article. If you have any questions regarding the topic, please feel free to ask in the comments. I will try to answer them with the best of my knowledge. I am, by no means, an expert in Node.js but I have read from multiple resources and combined the facts here in this blog. If you feel I have mistaken at any place, please feel free to correct me in comments.
Thanks a lot for reading.
Feel free to connect with me on Twitter/GitHub.
Have a good day! π
Top comments (26)
I love this explanation from Jake Archibald with great visuals.
I mention it in my frontend resources post with some other goodies
Frontend Developer Resources 2020
Nick Taylor (he/him) γ» Jan 6 '20 γ» 11 min read
It is great indeed! Thanks for sharing it here.
Hello nice article. I have clarifying question
process.nextTick()
happens after all the event loop phases or before the phases?Hi,
process.nextTick
callbacks are executed immediately. As mentioned, whenever the event loop encountersprocess.nextTick
, it finishes its current callback execution(no matter what phase it is in), then pauses and executes ourprocess.nextTick
callback first after which it resumes to the phase which it left its work on.I hope you this answers your question.
Thank you for the response sir. Last question it only happens in their execution context right? say I have this code:
output:
bar
immediate
foo
even though I called f() first before setImmediate
Hey, pardon for the late reply. Yes, it happens in the execution context. Thanks for reminding, I think I forgot to mention this term in the article! By the way, I think you may have mistaken for the output, after executing, the output seems to be:
Here is how it happens:
I never tried my code. I thought process.nextTick will be push to event loop and setImmediate will run next since its in the higher or outer execution context.
anyway thanks for explaining. :)
Every code gets executed inside execution context only right? then why that question was raised? Am I not understanding something? pls help!
Hi,
The blog is very helpful but the outputs of 2 small code snippets in your blog is where i am getting confused. Could you please tell me where I am going wrong ? Your help will be REALLY valuable. Im getting confused in process.nextTick() .
In the above code all the callbacks from microtasks queue are executed first so the below output :
2
4
3
1
But in the the last code snippet you mentioned as example :
}
main();
In the above code process.nextTick does not seem to be executed first as seen in the below output :
1
2
5
process.nextTick
4
3
close callback
Could you please explain why process.nextTick is not being executed first ??
I think because process.nextTick is in fs.readFile block, so when event loop comes to poll phase, it has to wait for i/o task is completed before executing anything else so every callbacks in this block are put to corresponding phase queue, now, there is nothing to do more in this phase, the event loop will move to check phase and print 5 to console, next iteration when event loop comes to i/o phase, it check that i/o task is done so it prints 'process.nextTick' -> check phase( print '4') -> closing phase -> timer phase (print '3')
Okay ! Did not read the code carefully that's y d confusion . Thanks a lot :)
Hey! Pardon for the late reply. As trunghahaha said, process.nextTick is wrapped inside the fs.readFile, hence, the event loop gets to know about it only when the callback of fs.readFile is executed, right? Hence, such behaviour.
One of the best explanation for the event loop.
Are the following will execute in the same fashion?
1.With main
function main() {
........Some Code.......
}
main();
OR
2.Without main
........Some Code.......
I really enjoyed this article. Clear and on point. Thanks for this.
Might I add, if anyone needs an in-depth answer on what 'tick' is, the following image can help.
image: dev-to-uploads.s3.amazonaws.com/i/...
src : stackoverflow.com/questions/198226...
author : josh3736
ππΌππΌ
Isn't it great how
setImmediate
is less immediate thannextTick
which isn't executed in the next tick but in the current? π€ͺYes, it is indeed. It is also said that they should have been named the other way i.e
setImmediate
should have been namedprocess.nextTick
and vice versa.Thank you, great one π
But I have a little question..
What's the difference between fs.readFile and fs.Promises.readFile , in other words where will be fs.Promises.readFile priority in the context of this post
Since, promises come under microtasks, as far as my knowledge,
fs.Promises.readFile
gets the priority but the only catch is that the handler passed to.then(fn)
i.e fn here is pushed to the queue (registered) only after the promise is resolved/rejected.Whereas, if it is a
fs.readFile
, its callback is immediately registered by the event loop when it(the event loop) encounters thefs.readFile
operation.Thus, if you do something like:
You may see that the output will be:
Hope this helps!
Thank you for replying π , but for this piece of code I put fs.promise.readFile upfront so it would(should) be resolved and pushed to the queue early before settimeout, can you clarify why this output .. !
Thank you for this amazing post ππ»ββοΈ
Hey Abdelrahman, Thanks for reading! Glad you liked it!
This is one of the best explanations of the Node.js event loop out there! Nicely done.
Hello!
It's a great article and I thoroughly enjoyed it!
One question though!
Can the maximum number of callbacks in a queue to be executed set by us for performance benefits? Or is it system defined?