Chaining using Callbacks

A callback is a function passed as an input parameter to a function, so that the function can use it for processing it’s data.

Here is an example where the array.forEach() is using sayHello or sendEmail as callback for processing it data. It does not know about these functions, but it is capable of calling them when passed as a parameter.

const clients = ['Robin', 'Ishan'];

function sayHello(name){
  console.log("Hello "+ name);
}
function sendEmail(name){
  console.log("Sending email to  "+ name);
}

clients.forEach(sayHello);
//outcome:
//Hello Robin
//Hello Ishan

clients.forEach(sendEmail);
//outcome:
//Sending email to  Robin
//Sending email to  Ishan

 

In a similar way, in case of asynchronous calls, we can use the callback as a follow up processor. Here, we can pass the callback as input parameter to a function, so that the function can pass its outcome for the follow up processing as shown.

Callback : The function with follow up processing logic

Before we see how to use these callbacks in more detail, lets first explore why do we need this ?

 

Callback – Why do we need this ?

Lets us look at our function – getCustomerAccountSummary. It has two logical parts where the part-2 depends on the outcome of the part-1 as shown. 

const customerData =[
    { id: 100, name:"Devasish Kulkarni", savingsAccId: 23415, dematAccId: '180-1576-7989'},
    { id: 101, name:"Akshyat Acharya", savingsAccId: 63715, dematAccId: '160-1876-9889'}
];
function getCustomer(customerId) {
    const customer = customerData.filter(item => item.id==customerId)[0];
    return customer;
}
function getAccountBalance(accountId) {
      console.log("Getting the account balance for account id: "+ accountId);  
}

function getCustomerAccountSummary(customerId){
  console.log("Fetching Account Summary for Customer ID : "+ customerId+"\n");

  //Part-1 :A call to get the customer information 
  let customer =  getCustomer(customerId);
   
  //Part-2 :A follow up processing using the result from part-1 
  if(customer != null){
    console.log("Customer : "+ JSON.stringify(customer));
    
    getAccountBalance(customer.savingsAccId);
  } 
}

getAccountDetail(100);

The above code is fine as long as the program is a synchronous piece of code.

Now what if the getCustomer(customerId) were an asynchronous call ? Let’s say something like below one.

function getCustomer(customerId) {
    const customer = customerData.filter(item => item.id==customerId)[0];
    setTimeout(()=>{ 
      return customer; 
    }, 1000);
}

const customer = getCustomer(100); //---- customer value will not be set

Such asynchronous calls won’t return its value inside the calling program. In fact, we do not want to wait for its response; we just make a fire-and-forget type of call. Then, how can we pass its outcome to it’s follow up processing i.e. the part-2 in our code ?

This is where the chaining mechanism comes into picture. The callback is one of them which we will be discussing here. We will discuss on the other two – promise and async-await in our next two articles.

Now let us restructure our getCustomer(customerId) so that it can call it’s follow up action using callback.

Callback – How does it work ?

The right side of the diagram shows the re-structured code to accommodate the callback.

Function(part-1) restructured to support Callback

Step -1 : Instead of returning the outcome, part-1( getCustomer ) will pass it’s outcome to the callback function which we will be providing as a parameter.

Step -2 : We are defining the callback function with the follow up processing as in part-2.

Thus, wherever the function in part-1(getCustomer) runs, as long as it gets part-2 as its callback, it will pass its outcome to it for the follow up processing. The executing thread or the time of execution will not matter anymore.

Now with the above approach the calling getCustomer will look something like below. Here, we are creating the Callback as in step-2 using anonymous function definition.

getCustomer(customerId, 
      (customer) =>{
         //Callback defining follow up processing as in part-2.

         if(customer!= null){
            console.log("Customer : "+ JSON.stringify(customer));
            getAccountBalance(customer.savingsAccId);
         }
      }
);

 

Including Error Outcome in our Callback

Similar to successful outcome, we can add the error as another parameter to the callback. Importantly, only one of the two parameters will have the outcome. By convention we usually keep the error as the first parameter.

Important lines :

  1. Line-10 : We are passing error to callback
  2. Line-12 : We are passing successful result to the callback
  3. Line-24 to 34 : We are defining the callback with the follow up action
const customerData =[
    { id: 100, name:"Devasish Kulkarni", savingsAccId: 23415, dematAccId: '180-1576-7989'},
    { id: 101, name:"Akshyat Acharya", savingsAccId: 63715, dematAccId: '160-1876-9889'}
];
function getCustomer(customerId, callback) {
    const customerList = customerData.filter(item => item.id==customerId);
    
    setTimeout(()=>{
      if(customerList.length==0){//Set data as null
        callback(new Error("Customer ID - "+ customerId +" does not exists!"), null);
      }else{//set error as null
        callback(null, customerList[0]);
      }
    }, 1000);
}
function getAccountBalance(accountId) {
      console.log("Getting the account balance for account id: "+ accountId);  
}

function getCustomerAccountSummary(customerId){
  console.log("Fetching Account Summary for Customer ID : "+ customerId+"\n");

  getCustomer(customerId, 
     (error, customer) =>{
        //Follow up processing using the outcome......
        if(error != null){
          console.log(error);
          return;
        }
        if(customer!= null){
          console.log("Customer : "+ JSON.stringify(customer));
          getAccountBalance(customer.savingsAccId);
        }
     });
}

getCustomerAccountSummary(100);

 

If we need more examples on the callback functions, we can refer the examples under the NodeJS File System APIs.

Callback Hell

As we saw, the callback solves the issue of ordering the dependent or the follow up process for asynchronous functions. But, one major problem with callback is the need to nest the dependent process.

The nesting makes the program complicated to manage as the request execution process runs beyond two or three asynchronous processing steps. The structure then looks somewhat as below, creating something know as Callback Hell.

firstCall(input, (error, result) => {
    if(error!=null){
      //handle error
    }
    //process result
    secondCall(input, (error, result) => {//first level nesting...
        if(error!=null){
          //handle error
        }  
        //process result
        thirdCall(input, (error, result) => {//second level nesting...
            if(error!=null){
               //handle error
            } 
            //process result
        });
    });
});
const fs = require('fs')
const testFile='test.txt';
const content = 'Some content!'
const appendContent = 'Some more content!'

fs.writeFile(testFile, content, (err)=>{
   if(err){console.error(err);}

   fs.readFile(testFile, 'utf8',(err,data)=>{
      if(err){console.error(err);}

      console.log("Content after write : \n" + data);
      fs.appendFile(testFile, appendContent,(err)=>{
        if(err){console.error(err);}

        fs.readFile(testFile, 'utf8',(err, data)=>{
          if(err){console.error(err);}

          console.log("Content after append : \n" + data);
      });
    });
  });
});

Thankfully we have a solution for this in Promise and Async-await that lets us write these nested asynchronous calls in a simpler way, similar to the sequential synchronous method calls. We will look at this in our next section.