DEV Community

Cover image for Understanding the Node.js event loop phases and how it executes the JavaScript code.
Sumedh Nimkarde
Sumedh Nimkarde

Posted on

Understanding the Node.js event loop phases and how it executes the JavaScript code.

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:

Alt Text

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.).

  1. 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).

  2. 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 error ECONNREFUSED, 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.

  3. Idle/Prepare: In this phase, the event loop does nothing. It is idle and prepares to go to the next phase.

  4. 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:

    1. 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.
    2. 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.
  5. 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.

  6. Closing callbacks: In this phase, the event loop executes the callbacks associated with the closing events like socket.on('close', fn) or process.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();


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.
  2. 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();


Enter fullscreen mode Exit fullscreen mode

The above code will always output:



2
1


Enter fullscreen mode Exit fullscreen mode

Let us see how the above code is executed:

  1. 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.

  2. 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.

  1. 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 before setTimeout(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();


Enter fullscreen mode Exit fullscreen mode

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 the microtasks queue until it is drained up. Once the queue is drained up, it returns back to the phase where it left its work from.

  1. It first checks the microtask queue and executes the callbacks in it(process.nextTick callbacks in this case).
  2. 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.
  3. It then goes to the 'Check (setImmediate)' phase where it sees the timer expired and executes the callback which logs '3'.
  4. 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


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

The output of the above code snippet is:



2
3
1
4


Enter fullscreen mode Exit fullscreen mode

Now, here is what happens when the above code is executed:

  1. All the callbacks are registered and pushed to their respective queues.
  2. 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.
  3. 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.
  4. It then enters 'Check (setImmediate)' phase and executes the callback of setImmediate which ultimately logs '3'.
  5. 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();


Enter fullscreen mode Exit fullscreen mode

Following gif indicates how does the event loop execute the above code:

Note:

  1. The numbers in the queues indicated in the following gif are the line number of the callbacks in the above code.
  2. 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.

Alt Text

The above code will output:



1
2
5
process.nextTick
4
3
close callback


Enter fullscreen mode Exit fullscreen mode

OR, it can also be (remember the very first example):



2
5
1
process.nextTick
4
3
close callback

Enter fullscreen mode Exit fullscreen mode




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)

Collapse
 
nickytonline profile image
Nick Taylor

I love this explanation from Jake Archibald with great visuals.

I mention it in my frontend resources post with some other goodies

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde

It is great indeed! Thanks for sharing it here.

Collapse
 
artoodeeto profile image
aRtoo

Hello nice article. I have clarifying question process.nextTick() happens after all the event loop phases or before the phases?

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde

Hi, process.nextTick callbacks are executed immediately. As mentioned, whenever the event loop encounters process.nextTick, it finishes its current callback execution(no matter what phase it is in), then pauses and executes our process.nextTick callback first after which it resumes to the phase which it left its work on.

I hope you this answers your question.

Collapse
 
artoodeeto profile image
aRtoo

Thank you for the response sir. Last question it only happens in their execution context right? say I have this code:

console.log('bar')

function f() {
 process.nextTick(console.log('foo'))
}

f()
setImmediate( ()=> console.log('immediate') )

output:
bar
immediate
foo

even though I called f() first before setImmediate

Thread Thread
 
lunaticmonk profile image
Sumedh Nimkarde

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:

bar
foo
immediate

Here is how it happens:

  1. console.log gets logged
  2. f is called. f()
  3. now, everything happens according to the f() context. i.e process.nextTick will execute with priority. If there are any other setTimeout, setImmediate callbacks, they will be pushed to the respective queues and executed.
  4. lastly, when the function returns, we come back to the last setImmediate, and execute the callback.
Thread Thread
 
artoodeeto profile image
aRtoo

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. :)

Thread Thread
 
kausachan profile image
kausachan • Edited

Every code gets executed inside execution context only right? then why that question was raised? Am I not understanding something? pls help!

Collapse
 
dhireneng profile image
dhiren-eng • Edited

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() .

function main() {
  setTimeout(() => console.log('1'), 50);
  process.nextTick(() => console.log('2'));
  setImmediate(() => console.log('3'));
  process.nextTick(() => console.log('4'));
}
main();

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 :

const fs = require('fs');

function main() {
 setTimeout(() => console.log('1'), 0);
 setImmediate(() => console.log('2'));

 fs.readFile('./xyz.txt', (err, buff) => {
  setTimeout(() => {
   console.log('3');
 }, 1000);

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

 setImmediate(() => console.log('4'));
});

setImmediate(() => console.log('5'));

setTimeout(() => {
 process.on('exit', (code) => {
  console.log(`close callback`);
 });
}, 1100);

}
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 ??

Collapse
 
trunghahaha profile image
trunghahaha • Edited

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')

Collapse
 
dhireneng profile image
dhiren-eng

Okay ! Did not read the code carefully that's y d confusion . Thanks a lot :)

Thread Thread
 
lunaticmonk profile image
Sumedh Nimkarde

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.

Collapse
 
ashutoshningot profile image
Ashutosh Ningot

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.......

Collapse
 
sanyaldips93 profile image
Dipayan Sanyal • Edited

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

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde

👌🏼👌🏼

Collapse
 
kaqqao profile image
Bojan Tomic

Isn't it great how setImmediate is less immediate than nextTick which isn't executed in the next tick but in the current? 🤪

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde

Yes, it is indeed. It is also said that they should have been named the other way i.e setImmediate should have been named process.nextTick and vice versa.

Collapse
 
abdallahmansorr profile image
Abdallah Mansour

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

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde • Edited

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 the fs.readFile operation.

Thus, if you do something like:

fs.promises.readFile(`./file.txt`).then((buff) => {
    console.log(`> resolved promise`);
  });


  fs.readFile(`./file.txt`, (err, buff) => {
    if (err) throw err;
    console.log(`> not a promise`);
  });

You may see that the output will be:

> not a promise
> resolved promise

Hope this helps!

Collapse
 
abdallahmansorr profile image
Abdallah Mansour • Edited

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 .. !



const fs=require('fs')

// text.txt file just contains 'Hello World'
const read=fs.promises.readFile('./text.txt','utf-8')

read.then(()=>{
    console.log('from promise')
})

//just looping to ensure that the file has finished reading
for(let i=0;i<10000;i++){
    console.log('looping...')
}

setTimeout(() => {
    console.log('from setTimeout')
}, 0);


//--output--
// looping...
// from setTimeout
// from promise


Collapse
 
ismail9k profile image
Abdelrahman Ismail

Thank you for this amazing post 🙋🏻‍♂️

Collapse
 
lunaticmonk profile image
Sumedh Nimkarde

Hey Abdelrahman, Thanks for reading! Glad you liked it!

Collapse
 
thawkin3 profile image
Tyler Hawkins

This is one of the best explanations of the Node.js event loop out there! Nicely done.

Collapse
 
sonalprabhu profile image
sonalprabhu

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?