Handling Async Operations in Node.js

Prachi
JavaScript in Plain English
7 min readNov 5, 2020

--

Photo by Clay Banks on Unsplash

After a lot of digging, I understood how node.js internally works. This blog is based on my researches, stay patient to deep dive!😁

Okay! So let’s understand first what Node.js is 🐘. The documentation says: “Node.js¼ is a JavaScript runtime built on Chrome's V8 JavaScript engine.”

A lot of fancy words here đŸ€Ż. Let’s together break it down đŸ’Ș First thing first, what is runtime and why node.js is a runtime ???

A runtime is any code that is executed to make your code work. The code that we write some code that will be executed by the runtime. A runtime can consist of a compiler, an interpreter, an engine to execute your code, thread pool to provide threads for your code’s process.

An engine is the actual code responsible to execute your program. It converts your code into machine code so that your computer(CPU) will execute it.

In JavaScript — The JavaScript engine translates your script into runnable machine code instructions so it can be executed by the CPU.

JavaScript Runtime Environment is just like a big container that contains other small containers like engine, heap, stack, web API, event loop, callback queue. As the JS engine parses the code it starts putting pieces of it into different containers.

The important thing is the JavaScript engine implementation is totally independent of the runtime environment. Engines aren’t developed with any particular environment in mind.

For example — The Chrome Browser and node.js use the same Engine — V8, but their Runtimes are different: in Chrome you have the window, DOM objects etc, while node gives you Buffers and processes.

Imagine a robot is cooking food:

  • Your code would be the instructions to the robot to cook food.
  • The engine would be the robot that can understand the instructions and act on it.
  • The runtime would be the LPG gas stove and the utensils.

Node.js is a runtime environment because it has V8 engine, libuv threadpool and OS Async Helpers. All of these gives you the power to write the JavaScript code on the server.

Coming to another fancy word: “V8 engine”🩋

The documentation says: “V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++.”

Well, it means, V8 is a C++ program, which receives JavaScript code, compiles, and executes it.V8’s responsibilities:

  1. Compiles and executes JS code.
  2. Handling call stack to run your JS functions in some order
  3. Managing memory allocation for objects — the memory heap
  4. Garbage collection of objects which are no longer in use
  5. Provide all the data types, operators, objects, and functions

Note:-V8 doesn't know anything about the Document Object Model (DOM) — which is provided by the browser

Let’s understand threads and processes before understanding the event loop.

Anything your computer does is done using a process. For example, a file is opened by a text editor process. Music is played by a music player process.

Similarly, in order to run your node program, a process is required. This process is created as soon as you run node in your terminal.

Each process has threads which actually execute the code inside the program of the process. Threads use your CPU cores to execute the code. A process can have more than one thread.

So, when you run the following code using node: const mul= 4*2; — A process is created and threads are assigned to the process to execute the code.

NodeJS process is not “single-threaded”. “Event loop” is single-threaded

Event Loop

Consider the following code:

console.log("This is the first statement");setTimeout(function(){
console.log("This is the second statement");
}, 5000);
console.log("This is the third statement");

Output

This is the first statement
This is the third statement
This is the second statement

Node.js doesn’t wait for 5 sec for the second statement, rather it executes the third statement and in parallel waits for the timer of 5 sec to expire despite being single threaded. How? Event Loop to the rescue!

“Event loop” is, well, just a loop like a for loop or a while loop. When you start your node process by running node app.js, the following steps take place:

  1. V8 engine executes your code in app.js
  2. V8 engine immediately starts “Event Loop”

So one thing should be very much clear now — Event loop doesn’t execute your code , V8 engine is responsible or executing your code.

“Event Loop” is some piece of code in a loop which picks up tasks from“Job/Task Queue” and sends it o the Call Stack to be executed.

Wait! Hold On! Which tasks and what is the “Job Queue” ?

So a Job Queue is FIFO data-structure. The tasks which are pushed first will be picked up first by the Job Queue.

The tasks here is any code inside the function that needs to be executed. When I/O operation, setTimeout, setInterval, setImmediate, or an OS task completes, callback function is called, which is then entered inside the Job Queue. It will ‘wait’ until the Stack is completely empty. When the Stack is empty it will send the callback function at the beginning of the queue to the Stack.

As I mentioned above, event loop is like a for or while loop. It has certain phases through which it iterates — it is called “Event Loop Iteration”.

Each of the phases has its own 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.).

Phases are:

1. Timer

The callbacks of timers(setTimeout, setInterval) in JavaScript are kept in the heap memory until they are expired. If there are any expired timers in the heap, the event loop picks up the callbacks associated with them and takes it to the call stack.

Technically, 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

Certain system-level callbacks are executed during this phase. You don’t really need to worry about this phase (and honestly there is not very much information to be found about it), but I wanted you to know it’s there.

3. Idle/Prepare

In this phase, the event loop does nothing. It is idle and prepares to go to the next phase.

4. Poll Phase

This phase is the one that makes Node.js unique. In this phase, the event loop watches out for new async I/O callbacks and executes the pending I/O(fs.read file()) callbacks. Nearly all the callbacks except the setTimeout, setInterval, setImmediate and closing callbacks are executed.

The poll phase has two main functions:

  1. Calculating how long it should block and poll for I/O, then
  2. Processing events in the poll queue.

When the event loop enters the poll phase and there are no timers(setTimeout, setInterval) scheduled, one of two things will happen:

  • If the poll queue is not empty, the event loop will iterate through its queue of callbacks executing them synchronously until either the queue has been exhausted, or the system-dependent hard limit is reached.
  • If the poll queue is empty, one of two more things will happen:
  • If scripts have been scheduled by setImmediate(), the event loop will end the poll phase and continue to the check phase to execute those scheduled scripts.
  • If scripts have not been scheduled by setImmediate(), the event loop will wait for callbacks to be added to the queue, then execute them immediately.

Once the poll queue is empty the event loop will check for timers whose time thresholds have been reached. If one or more timers are ready, the event loop will wrap back to the timers phase to execute those timers’ callbacks.

5. Check/setImmediate

setImmediate() is actually a special timer that runs in a separate phase of the event loop. It uses a libuv API(will talk about it) that schedules callbacks to execute after the poll phase has completed.

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

“libuv” is a library that provides a set of threads(thread pool) to NodeJS runtime to execute long-running tasks, like fs module-based tasks. By default, it provides 4 threads to each node process but you can change the thread pool size by setting process.env.UV_THREADPOOL_SIZE to any value you want.

“OS Async Helpers” are used when any low-level OS operations take place. For example, a REST API call using http or https module or creation of a web server using http.createServer(). These operations never use the thread pool and are executed immediately as soon as a CPU core is available for operation.

Once “libuv” or “OS Async Helpers” are done with their tasks, they put the callbacks into the “Job Queue” for the “Event Loop” to pick them up and then send them to “V8 Engine” to be executed.

Example

Output

2
1

Let’s see how Node.js executes this code:

  1. When main() is called, fs.readfile() is encountered first with a callback. It’s callback is pushed inside the I/O phase queue because fs operations are I/O operation.
  2. When event loop sees that the file reading operation is complete, it starts executing the callback, after which, it moves to the Check(setImmediate) phase.

Check phase comes before the Timers phase .Hence, when in I/O phase, the callback of setImmediate will always run before setTimeout(fn, 0).

The main advantage to using setImmediate() over setTimeout() is setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.

Thank you for reading! You can follow me on Medium and LinkedIn. Feel free to ask any questions in the comments.đŸŒŒđŸŒŒđŸŒŒ

--

--