DEV Community

Ishan
Ishan

Posted on

Nodejs Event Loop: A Comprehensive Overview

In this article we will review the asynchronous nature of nodejs event loop. Asynchronicity is one of the key features of nodejs which allows us to build highly scalable servers.
Primer
What is Nodejs? It is javascript runtime environment. Originally javascript was meant to be run on browsers. Before nodejs, only place you could run your javascript code was in browsers. When javascript started gaining popularity development teams behind major browsers worked hard to offer better support for javascript and find ways to run javascript faster. This led to the development of V8 engine from The Chromium project. V8 engine’s only job is to execute your javsascript code. Nodejs uses this engine to execute javascript code.
So, if both, the browser and Nodejs uses V8 engine then what’s the difference between the two?
Difference between Nodejs and the Browser?
V8 engine is a small part of this story. Surrounding it, there is a handful of functionality.
Browser
Our javascript code can invoke web apis such as DOM, XMLHttp, etc. to do specialized functions such as manipulating the DOM (using the global document object), making an http request (using the fetch function) or saving a JWT(using the localStorage object) (more web apis on official mdn). Keep in mind that web apis has nothing to do with the ability of browsers to run javascript code, which is done by V8 engine.
f

V8 engine invoking web apis
Nodejs
Since nodejs is a javascript runtime environment, it provides you the ability to run javascript code outside of a browser. Umm… then what about the web apis which provides so many functionalities? We certainly won’t have access to web apis (such as document or fetch) because there is no browser. Instead we get another set of awesome apis to handle all our asynchronous tasks. Some of these are:

  • File system ( fs)
  • Network ( http)
  • Child processes ( child_process)
  • DNS ( dns )
  • and many more… (libuv design) s

V8 engine invoking libuv apis
Event Loop
Well, it is essentially a loop. It has the sole responsibility to determine what functions/code to run next. In other words, it checks if the call stack is empty (there is no function execution context in the call the stack except for global exectution context) and pushes the function into call stack from the callback queue.
This was a lot of technical jargon. Let’s break it up…
Call stack
Javascript keeps track of which function is being run, whenever a function has to be run it is added to the call stack. More specifically, a function execution context is added to the call stack. A function execution context contains all the local variable definations. A global execution context contains all the variables defined globally.
Callback queue
Any functions delayed from running are added by node itself to the callback queue when the corresponding background task has been completed. Node will check if there is anything to be run in the callback queue, then checks whether the call stack is empty, if it is, node will itself push the function to the call stack. This is really weird compared to other languages, but this allows node to perform asynchronous task in a non-blocking way. This will be more clear in the coming section.
The Big picture
In this section we will briefly understand when and what is pushed to call stack from callback queue.
Since nodejs is pushing (or invoking) functions to call stack for us, it has to be very strict about when these functions are allowed to execute.
Guess the output of this code:
t

Rules of event loop

  • Only after the regular (synchronous) javascript code has completed running, shall the asynchronous code run. Remember what we discussed in the earlier section, call stack must be empty (except for global execution context), then and only then deferred functions shall run. State of runtime evironment at line 20. setTimeout is called with helloWorld function defination and number 0. setTimeout is just a wrapper in javascript which calls timer api in libuv. Now, node is checking continuosly has 0ms passed (technically it is maintaining a min heap), and when 0ms are complete then node takes the function defination helloWorld as is and queues it in the timer queue. But is the function allowed to be pushed in the call stack? No, remember functions from the callback queue will only be pushed once the regular javascript code has finished runnning. Even if the timer has completed it’s callback won’t be allowed to run. fo

At line 27, our node program outputs to the console:
499999500000
first
Similar to setTimeout, readFile is just a wrapper function around the c++ version of readFile. Libuv takes the function defination, sets up another thread for reading the file and when it’s done it takes the function defination parseData and queues it to another queue called the I/O queue.
Same for setImmediate, it also takes the function defination immediate and queues it to yet another queue called the check queue.
State of runtime environment after runnning setImmediate:
fi

The I/O callback queue is empty since libuv sets up a thread from its thread pool and starts reading the file. Only when it has read 64 kb(default buffer size) parseDate shall be queued in I/O queue.
Now its time for celebration that our regular javascript code has finished running. We can now dequeue stuff from these queues which brings us to second rule, the priority of these queues

  • Priority Queue: After running the regular javascript code, there might be a bunch of deferred functions held up on these queues. Priority of a queue over another is essential for node’s deterministic nature.

Here is the priority from highest to lowest:

  • Timer Queue
  • I/O queue
  • Check queue Now you can guess the output to the console. Check is meant for running functions immediately after all the queues have been exhausted. Hence, this is the output to the console: 499999500000 first Hello world! Run immediately [ { df: ‘fdf’, eR: ‘fs’ } ] //data.txt

Bonus

Well, this is not all of it. There are 2 more callback queues with different priorities. Let’s update our list queues from highest to lowest priority:

  • Microtask queue: — — process.nextTick() — — Promises
  • Timer queue
  • I/O queue
  • Check queue
  • Close queue

Now you know, microtask queue has the most priority over any other queue. It is further divided into 2 queues. Functions passed (or deferred) to process.nextTick() are queued in this queue and functions deferred using the Promise api are queues in the promise queue. Note the promise queue has less priority then process.nextTick() but more priority than Timer queue.
The close queue is filled when ‘close’ events are fired (for example when closing a file).
Coming up next: Promises: A Comprehensive Overview.

Top comments (0)