Tuesday, April 14, 2020

Node.js callbacks

Node.js callbacks

Everything inside the Node runs upon a single-thread, and this thread must never be blocked.Node.js is a minimal JavaScript framework, that runs on the server-side. Node has the capability to execute asynchronously, and this behavior comes from the callbacks. Node doesn't wait around for things like file I/O to finish or any other blocking task being an asynchronous platform. In JavaScript, the functions are the first-class objects, and it means that we can use them like other objects (String, Arrays, etc.), even we can pass them as the arguments to the other functions.

The first thing we need to understand is that we can write a function as


function from(name)
{
return name;
}
function message(callback)
{
     var name=callback;
console.log('Welcome message from '+ name);
}
message(from('pushpendra'));
/*
Welcome message from pushpendra
*/


Callback


Generally, a callback can be defined just as the function that is passed to other functions as an argument. It is called upon the completion of a given task. There is no blocking or wait for I/O, during this time other code can be executed.

//Traditional I/O
var  result     =    db.query('select something from mytable');
doSomethingWith(result);   
//wait for result!        
doSomethingWithOutResult();     
//execution is blocked!

//Non-traditional, Non-blocking I/O(Node-style)
db.query('select something from mytable',function(result){      
doSomethingWith(result);   
//wait for result!   
}); 

doSomethingWithOutResult();    
//executes without any delay!     


We can take an example, to read the data from a text file, as this will require file-I/O operation. We can use the 'fs' module for this purpose. The fs module is used to handle the file-handling operations,


//Synchronous operation
var f=require('fs');
//welcome.txt is text file in same directory
var message=f.readFileSync("welcome.txt").toString();
console.log(message);
console.log("Program finished");    

/*
Output:
welcome to Node.js
end of the program
*/

 The same operation can be executed, asynchronously using a callback

//Asynchronous operation
let fs = require("fs");
 //welcome.txt is text file in same directory
fs.readFile('welcome.txt', function (err, result) {
   if (err) return console.error(err);
   console.log(result.toString());
});

console.log("Program finished");
/*
Output:
Program finished
welcome to Node.js
*/



A typical Node callback operation can be done as,

//Asynchronous operation
function doSomething (callback) {
       getSomeData(function (err, data) {
         if (err) {
           console.log("An error has occurred. Abort everything!");
           return callback(err);
         }
         //do something here
         callback(data);
       });
     }





The callback provides an interface that says "and when you're done doing that, do all this". This makes us perform as many IO operations as the Operating System can manage and handle at any moment of time.

Defining an error-first callback


This is known as “error-first” callback (also known as an “errorback”, “errback”, or “Node.js style callback”). There are two major rules for defining an error-first callback,

  • The first argument is kept for an error object in the callback. In case of error, the error object will be returned by this argument. 
  • The second argument is reserved for the successful response data object. If there is no error then the error object will be set to null and the data object will be returned in the second argument.

Asynchronous execution


We should not forget that the file system is not part of the Node. The file system belongs to the host operating system. So it may take unpredictable time to execute the IO operation with a file. One more important thing, that must be observed here in the asynchronous execution is,

Asynchronous Execution node
Asynchronous Execution

                     
The line of code (console.log("completed") ) is executed before the readFile() function. This is evident from the above example that the IO operation is not blocking the execution of the remaining code.

Callback hell


Callback hell is a situation encountered when we try to execute multiple asynchronous operations one after another and nesting them, within each other, eventually leading to very error-prone, difficult to read, handle, maintain and complex code.

//Asynchronous operation
var fs=require('fs');
fs.exists("welcome.txt",function(exists){
     if(exists)
     {
           fs.stat("welcome.txt",function(err,stats){
                if(err)
                {
                     console.log("Some error occured");
                     throw err;
                }
                if(stats.isFile())
                {
                     fs.readFile("welcome.txt","utf8",function(err,data){
                     console.log('reading the file...');
                     if(err)
                     {
                           console.log("Some error occured");
                           return err;
                     }

                     var txt=data.toString();
                     console.log('File contains message->'+txt);
                                });

                }
           });
     }
});
console.log("completed");
/*
Output:
completed
reading the file...

File contains message->welcome to Node.js
*/


Avoiding the Callback hell

This situation can be avoided by using small named functions as callbacks instead of using anonymous nested functions. For example, we can create three different functions testExists(), testStat() and testReadFile() as named function, in place of anonymous functions.


//Asynchronous operation
var fs=require('fs');

function testReadFile(error,data)
{
     if(error)
     {
           console.log('Some error occured');
           throw error;
     }
     console.log(data);
}

function testStat(error,stats)
{
     if(error)
     {
           console.log("some error occured");
           throw error;
     }

     if(stats.isFile())
     {
           fs.readFile("welcome.txt","utf8",testReadFile);
     }
}
function testExists(exists)
{
     if(exists)
     {
           fs.stat("welcome.txt",testStat);
     }
}

fs.exists("welcome.txt",testExists);


console.log("completed");

/*
Output:
completed
reading the file...

File contains message->welcome to Node.js
*/


The disadvantage of breaking code into several small named functions is larger code length. For small applications it may be harder to follow but for larger applications, this is essential for overall application architecture.

Reference:https://nodejs.org/en/knowledge/getting-started/control-flow/what-are-callbacks/