How the Node.js event loop works
You might have heard before that JavaScript is a single threaded programming language. This statement is not 100% true, but we will consider one thread throughout this post.
What is a thread?
The thread can be considered as a list of code (functions, declarations etc.) that needs to be executed, one line after the other. Unfortunately some of these code instructions are time and resource consuming, and if they run after each other, the one that runs earlier would block the execution of the latter.
This simply means that functions that are written later in the code will execute after the earlier functions have already run.
We are often required to run expensive operations in Node.js. The mechanism called event loop helps us manage these situations.
Node.js and the event loop
Although Node.js has some functions that use the thread pool to improve performance and therefore we can’t say that Node.js is entirely single-threaded, the event loop is an important aspect of the language, which cannot be ignored.
For sake of simplicity, we will assume that we have a single process running with a single thread and will examine how the event loop behaves in this environment.
How the event loop works
When we run a function with the node FILENAME.js
command, Node.js starts a process. We can imagine that this is a large function which is called main
.
When this main
function starts running, the code inside (i.e. the code we type in our file in our editor) will execute line by line, which means that the code is read from top to bottom.
Now let’s say that we have a function which needs to run five seconds after the line of code is read for whatever reason.
What would happen if we didn’t have the event loop?
When the code is read line by line and we get to this piece of code, the execution of main
(i.e. the process) would stop and hang there for five seconds. This line would block the execution of the rest of the code.
And it’s quite a big deal. What if it’s not five seconds but five minutes? The process would hang for that amount of time before it resumed!
This is when the event loop comes in the picture.
When the process (the main
function), i.e. the code we have written starts executing, Node.js will take note of these problematic functions and methods that would block the execution of the rest of the code.
These are usually methods where time, therefore blocking would be an issue. For example:
-
Timer functions, like
setTimeout
,setInterval
orsetImmediate
. These functions help us run a piece of code at a later point of time, which means that they could block the process. -
Operating system tasks, like server requests. In case of a
GET
request, is it guaranteed that we will get the response immediately? Not really. It cannot be guaranteed that the remote server we want to fetch the data from will promptly respond. It might have a few seconds of delay in the response. It might not be working at all. Who knows? Similarly, when we want toPOST
something, we might not be able to save our new data immediately. - Other tasks like reading from and writing to the file system.
Node.js will take note of such functions and methods and then the process continues with the line after. After the last line of code has run, Node will get back to the first method or function that got recorded, and the event loop starts.
These are the methods that accept everyone’s favourite concept: a callback function. Callback functions, as their name suggests, run when certain conditions set by the parent function have been met. In case of setTimeout
, the callback will execute after the time (say five seconds) has elapsed. When a GET
request is made, the callback function will run after the response has arrived.
To cut it short, Node.js will take note of the functions and methods that has the potential of blocking the process and will start running their callbacks inside the event loop after the rest of the code has been executed.
Why loop?
But why is it called a loop? In a well-behaved, healthy Node.js application more of these blocking functions and methods can occur. Each of them can be nested. For example, once the response arrives from a GET
request, we can write the data to the file system. Or, we can POST
it somewhere else.
So the event loop has to run again because we implemented new blocking methods at the next, nested level. Node.js will again take note of them and if such methods are found again, the event loop will restart, this time with the new callback functions running.
One round of the event loop is called a tick
, so the callbacks will run in the first tick. If another blocking function can be found at this level (i.e. we have some nested callbacks), they will be executed in the next round of the event loop, i.e. in the second tick. The same applies to the next level of callbacks and so on and on, until we get a nice layout of code called the callback hell.
Note that the use or promises or async .. await
syntax will not change the way the event loop works.
Example
As an example is worth a thousand words or something like that, let’s see in practice how the event loop works.
I created a file called event-loop.js
and put the following code inside:
console.log('First');
console.log('Second');
setTimeout(() => {
console.log('Third'); // 1st event loop tick
console.log('Fourth'); // 1st event loop tick
setTimeout(() => {
console.log('Fifth'); // 2nd event loop tick
}, 0);
}, 0);
process.nextTick(() => console.log('From tick: first')); // 1st event loop tick
setTimeout(() => {
console.log('Sixth'); // 1st event loop tick
process.nextTick(() => console.log('From tick: second')); // 2nd event loop tick
}, 0);
console.log('Seventh');
setTimeout(() => {
console.log('Eighth'); // 1st event loop tick
process.nextTick(() => console.log('From tick: third')); // 2nd event loop tick
setTimeout(() => {
console.log('Ninth'); // 2nd event loop tick
process.nextTick(() => console.log('From tick: fourth')); // 3rd event loop tick
}, 0)
}, 0);
process.nextTick(() => console.log('From tick: fifth')); // 1st event loop tick
console.log('Tenth');
There’s nothing fancy here. I used setTimeout
everywhere, simply because it’s easy to use and there’s no need to connect to another servers or to the file system. The methods we run are simply console.log
s.
The logs refer to their position in the code from the top. console.log
written first will log First
, then Second
etc.
One exception is process.nextTick
, where we log From tick:
and the position of the log from the top. More on this method later.
The main
function I mentioned above is basically the whole file, and when we run it with node event-loop.js
from the project folder (i.e. the process starts), we will get the following output written to the console:
First
Second
Seventh
Tenth
From tick: first
From tick: fifth
Third
Fourth
Sixth
Eighth
From tick: second
From tick: third
Fifth
Ninth
From tick: fourth
Let’s go over the code step-by-step.
Analysis of the result
Code is read from top to bottom, hence First
and Second
will be the first two things written to the console. Then comes a setTimeout
, which is a timer function. Although I put 0
milliseconds after which the callback function has to run, setTimeout
is a potentially blocking function, so it will get taken note of by Node, its whole block will be skipped for now and the next line is read:
process.nextTick(() => console.log('From tick: first'));
process.nextTick()
nextTick
is a method of the process
module available in Node.js. It accepts a callback function, which will be executed in the same round (i.e. tick) of the event loop. The speciality of this method is that the content of its callback (console.log('From tick: first')
) will run before the next round of the event loop.
Back to the code
Because process.nextTick
is again an event loop method, it will be recorded by Node.js and we move on to the next line. It’s a setTimeout
again, the same happens as above: Node takes note of it, and moves on to the next line, which is a console.log
. Luckily, this method is not blocking, so Seventh
is written to the console.
Then the same happens next, the presence of setTimeout
is recorded and Tenth
is logged.
We have run out of “normal”, potentially non-blocking methods and reached the end of the first level of functions, so the first round of the event loop (the second level) can start.
We had two process.nextTick
s, the first and the fifth one from the top at the first level. They will be executed first (but from top to bottom compared to each other) in the event loop, so we will get From tick: first
and From tick: fifth
logged. This is when the event loop starts.
Now that Node has dealt with nextTick
, it goes back to the very first potentially blocking function (setTimeout
), and starts executing its content. Both console.log
s will run in order, so the next two logs will be Third
and Fourth
. Still inside the first setTimeout
we have another one, so Node will record its presence, and set it aside for the next (second) round of the event loop.
Moving on to the next setTimeout
, Sixth
will be logged from the console.log
and a process.nextTick
gets taken note of as the first thing to do in the following round of the event loop.
The same happens in the third setTimeout
block, Eighth
is logged, process.nextTick
and the nested setTimeout
are recorded.
The second tick
The first round (or tick) is done, now Node.js checks if there are any more timer functions or other “even loop methods” left.
Yes, we have some. process.nextTick
s written at the last level inside some setTimeout
s get executed first (From tick: second
and From tick: third
), starting the second round of the event loop. They are followed by Fifth
from the first nested setTimeout
and Ninth
from the third one. No more code left to execute in this round, but - why not - another process.NextTick
is waiting for execution.
And so on
From tick: fourth
is logged last as it marks the start of the third round (tick) of event loop.
No more functions and methods left, but it could be continued with additional levels of nesting, the principle will always be the same.
A question can be asked: If we had server requests, file system tasks and timer functions, can we determine in which order they are executed inside the event loop?
The answer is yes, we can (some timer functions, operating system tasks, file system tasks), but it’s beyond the scope of this post as it requires deeper analysis. There are also situations which are also managed in the event loop but not mentioned in this article.
Conclusion
The event loop is one way how Node.js deals with potentially blocking code. It’s an important and complex concept, which can be scary at first but a systematic approach can help one understand how it works.
I hope the explanation above was clear and you have found this post useful. If so, see you next time.