Understand the Node.js Event-Loop
In this article we’re going to explore the secrete behind the terms “asynchronous”, “event-driven” and “nonblocking I/O”. By the end you will not only have a good understanding of the Event-Loop but also know why Node.js is not so single threaded. A nice side effect will be to finally understand proccess.nextTick() and setImmediate().
To understand the Event-Loop we first need a basic understanding of Node.js. Node.js is an asynchronous event-driven JavaScript runtime. Basically what Node does is providing us an easy-to-use consistent JavaScript API for all the complex stuff like networking, file operations, streams, etc. Node does this by connecting our JavaScript code to the C-libraries through a wrapper layer. The two most important parts are the v8 JavaScript engine which runs the JavaScript code and libuv which provides most of the functionality to deal with I/O. Now that we have a basic understanding what provides all the functionality let’s take a look at how this mystical asynchronous nonblocking behavior is achieved.
For this we mainly have to thank libuv and it’s event-driven asynchronous I/O model. To achieve this it uses the Event-Loop. So what is the Event-Loop. It’s a loop listening for events! Boom Event-Loop explained. But no lets take a look at the following diagram and go through it step by step.
It all starts with your JavaScript code. Every function that is called is pushed on the call stack. The call stack is part of the v8 engine and there the code gets executed. That is the single threaded part, only on thing can be processed at a time. But if the function on top the call stack is asynchronous the C++ wrapper does it’s magic and calls the underlying C code. The async function get’s removed from the call stack and gets registered (referenced) in the event-loop. Depending on what function you called it’s executed in a worker thread (by default there are 4) or handled by the OS. libuv will use the best mechanism available depending on the OS. Now that there is something referenced, the event-loop starts running. It uses a pattern of checking if something is due or happened and then adds the callback to the message queue. After checking for the current task all callbacks registered with process.nextTick() are added to the message Queue and then the same for all fulfilled promises. All callbacks added to the message queue get unreferenced on the loop. From the message queue the callbacks are pushed to the call stack if the call stack is empty. There they finally get executed. This pattern is repeated several times on each iteration of the loop. Let’s see what the different „checkpoints“ are.
- Check if timers are due: setTimeout() and setIntervall(). If so add the callback to the message queue.
- Next up are pending Callbacks. Most I/O callbacks get executed right after the I/O polling but if one was scheduled for the next iteration it gets added to the message queue from here.
- Then there comes the I/O polling. In this phase libuv checks if something for the registered I/O operations happend. E.g. if there is a new connection or if there is new data from a file read or write. If there is something “new” same thing will happen. The callback get’s added to the message queue.
- Now all callbacks registered with setImmediate() will be added to the message queue.
- Finally all “close” event callbacks are handled
Keep in mind that callbacks of process.nextTick() and fulfilled promises are added to the message queue after each „checkpoint“. Now the end of one iteration is reached. Here the event-loop checks if there are still some callbacks referenced (ref > 0) if there are any the next turn begins if no the loop stops and if the call stack is empty too, your program will exit with the “exit” event.
What to keep in mind regarding all that?
- Node is nonblocking because time consuming I/O operations are not handled by the main thread
- Node is asynchronous because some code ( callbacks ) is not directly execute but only on the occurrence of “events”. The mechanism to achieve this is the Event-Loop
- the Event-Loop has different “checkpoints” and there is an order in the execution of your callbacks
- not everything asynchronous is handled by worker threads, libuv is smarter than that
Know you should have a basic understanding of how the event loop works and why Node.js is not really single threaded. Of course it is a bit more complex than explained in this article and there is always more to learn and to discover. Good resources would be:
- the libuv documentation
- Everything You Need to Know About Node.js Event Loop — Bert Belder
- Node’s Event Loop From the Inside Out — Sam Roberts
Beware that the terminology used in the article may not always be correct but I tried to keep it as simple as possible.