Blocking and Non-Blocking in Node.js - Asynchronous Operations and Callbacks

The asynchronous coding paradigm enables us to write non-blocking code. This makes the single threaded Javascript run with efficiency. A single thread is like an execution that can do only one thing at a time. Node.js smartly uses this single thread to get non-blocking execution. We will understand blocking and non-blocking now.

function doTask1(){
 // do something here
}

function doTask2(){
 // do something else here
}

// perform some task
doTask1()
doTask2()

In this example code doTask1() function executes first and after it returns then doTask2() function executes. This is not a blocking code in javascript even if doTask1 takes a long time before returning (it could be performing some CPU intensive tasks like finding the inverse of a matrix).

Blocking

When javascript execution in Node.js process (each program is a process) has to wait until a non-javascript operation completes is called blocking.

Non-Blocking

This is the opposite of the blocking i.e. javascript execution do not wait until the non-javascript operation completes.

Non-Javascript execution refers to mainly I/O operations. So, in the nutshell, I/O operations are blocking.

I/O refers primarily to the interaction with the system's disk and network.

Now, let's take another example:

function doTask1(){
 const users = getAllUsers() 
 // do something with users here
}

function doTask2(){
 const services = getAllServices()
 // do something with services here
}

// perform some task
doTask1()
doTask2()

In this example, doTask1 internally calls getAllUser which makes the database connection and fetch the list of users. Also, doTask2 internally calls getAllServices which makes an HTTP request to get the list of available services of some 3rd party through their API.

Here doTask2 will be blocked till doTask1 returns because a single thread can execute only one thing at a time.

Now, you must be wondering that how Node.js converts this blocking calls into a non-blocking execution.

Node.js uses the event loop and callback mechanism to lift off I/O operations from the javascript's thread to the system's kernel.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background concurrenlty. When one of these operations completes, the kernel notifies Node.js and then the appropriate callback is eventually executed.

lets see how we can make this blocking code into non-blocking in Node.js using callbacks.

function doTask1() {
 const users = getAllUsers((err, data) => {
   // do something with users here
 })
}

function doTask2() {
 const services = getAllServices((err, data) => {
   // do something with services here
 })
}

// perform some task
doTask1()
doTask2()

Here, getAllUsers and getAllServices now take callbacks. These callbacks are then used by the Node.js and called when the Kernel is finished with the I/O operations.

The structure of callback in Node.js

A callback is a javascript function, which is called at the completion of a given task. Node.js has some convention for this callback function as listed below:

  1. The callback is passed as the last parameter to any function.
  2. The callback gets called after the function is done with all of its operations.
  3. The first parameter of the callback is the error value. If there is no error then the first parameter is set to null and rest being the return value.
function doTask1(callback) {
 const users = getAllUsers((err, data) => {
   // do something here
   if (err) return callback(err)
   // do something more here
   callback(null, data)
 })
}

You must have understood how Node.js is able to make asynchronous calls for non-blocking operations using callbacks. But even more interesting thing to understand is how callbacks work w.r.t I/O and when it is exactly executed. We will understand this when we explore the event-loop.

Node.js takes a strong stand for concruency using single threaded system. It advocates that threads based concruency is relatively inefficient and very difficult to use. Using a single thread also prevents the users of Node.js from worries of dead-locking (2 or more threads wait on the locks in a cyclic manner and non of them are able to free the locks) the process, since there are no locks.
Node.js makes the framwork very fast and efficient and one of the reason is that, no function in Node.js directly performs I/O, so the process never blocks. The callbacks used in these asynchronous calls allows you to have as many I/O operations as your OS can handle, happening simultaneously.

Many functions in the Node.js provides both blocking and non-blocking variants.

  1. The non-blocking version takes a callback function as a parameter.
  2. Some blocking counterpart names generally end with Sync. These functions execute synchronously and block the code.

Example of using the file system in both modes:

Synchronous file read:

const fs = require('fs');
const data = fs.readFileSync('/file.html'); // blocks here until file is read
console.log(data);
doSomethingElse(); // will run after console.log
Note: In synchronous version if an error is thrown then it will have to be caught else the process will crash.

Asynchronous file read:

const fs = require('fs');
fs.readFile('/file.html', (err, data) => {
 if (err) throw err;
 console.log(data);
});
doSomethingElse(); // will run before console.log
Note: In asynschronous version author is responsible for choosing the way of error handlling. We should always choose non-blocking version over the blocking varient of the function.

In the next article, I will discuss the event-loop, to understand Node.js concurrency in more detail.