Chaining with Promise

Callback Vs Promise Code Structure

When we do not want to wait on the function to complete, we can run it asynchronously in a fire-and-forget mode.

In our article on callback we saw how can we use a callback to continue with our follow up action after the asynchronous call is over. But, we also saw how the approach becomes complicated due to nested calls when we use multiple dependent calls.

The Promise implementation provides a much cleaner solution by keeping the dependent calls in the call chain separate and sequential.

As shown in the diagram above, with promise functions the code structure looks more closer to our sequential synchronous code, except for few additional syntax like .then().

How does it work ?

 

function myPromisifiedFunction(input){
   // Process the input or make an IO call with input data

   return new Promise((resolve, reject) => {   
       if (successful){                
               resolve(data); 
       } else {
               reject(error);    
       }
   });
}
function myFunctionWithCallback(input, callBack){
    // Process the input or make an IO call with input data
        
       if (successful){                
               callBack(null, data); 
       } else {
               callBack(error, null);    
       }

}

The promise implementation as shown above always returns the outcome in a promise object. Importantly, it use different callbacks- resolve and reject for setting a successful and failed outcome.

Depending of what we set inside promise, it triggers specific promise methods like .then(), catch() as shown. Hence, these methods allow us to define our next steps as the promise function completes.

Promise : How to chain and how to define our next Steps.

 

Understanding – Outcome Vs Promise Methods

Let’s look at this simple promise function to understand the relationship between the outcome and the promise methods for setting the next steps.

testPromiseCallback is a promise function that returns the outcome in a promise object at the end of the call. For successful outcome we use resolve; for failed outcome we can use reject or throw for sending the error message.

const testPromiseCallback = ()=>{
  //do the processing here and return the promise setting the outcome. 
  return new Promise((resolve, reject) => {
     // Different calls and their expected output
     resolve('Passed!');   //Passed
     //reject('Something went wrong!');    //Error message : Something went wrong!
     //throw 'Something went wrong!';      //Error message : Something went wrong!
  });
};

testPromiseCallback
.then((data) => { console.log(data); })
.catch((error) => { console.log("Error message :" + error); })
.finally(()=>{ console.log("Finally will be called irrespective of the outcome."); });

As we can see depending on the successful or the error outcome, the promise invokes the .then or .catch handler respectively.

We can use .finally for any clean up activities as the code will traverse through it mandatorily.

We may also handle both data and error inside the .then() itself as shown below with comma separated functions:

const testPromiseCallback = ()=>{
  //do the processing here and return the promise setting the outcome. 
  return new Promise((resolve, reject) => {
     // Different calls and their expected output
     resolve('Passed!');   //Passed
     //reject('Something went wrong!');    //Error message : Something went wrong!
     //throw 'Something went wrong!';      //Error message : Something went wrong!
  });
};

testPromiseCallback
.then(
  (data) => {console.log(data);},
  (error) => {console.log("Error message :" + error);}
 )

 

Exploring Different Aspects with more Examples

Having seen the basic apis and how a promise function works, lets now explore on various ways we can use it for chaining multiple dependent calls. But, to start with let us compare the promise code structure against the callback to realize how it simplifies our code.

 

1: Code Structure : Promise vs Callback

Here is a comparison of the code structure while we use 4 sequential asynchronous call as follows :

  1. Write some content to a file.
  2. Read the content after successful write.
  3. Append some more content.
  4. Read the file again after the append is complete.
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)=>{
        console.log("Content after append : \n" + data)
      });
    });
  });
});
const fs = require('fs').promises;

const filePath='test.txt';
const content = 'Some content!';
const appendContent = 'Some more content!';

fs.writeFile(filePath, content, 'utf8')
.then(()=>{
  return fs.readFile(filePath, 'utf8');})
.then((data)=>{
  console.log("Content after write : \n" + data);
  return fs.appendFile(filePath, appendContent, 'utf8');})
.then(()=>{
  return fs.readFile(filePath, 'utf8');})
.then((data)=>{
  console.log("Content after append : \n" + data);})
.catch((error) => {
  console.error(error);});

console.log("Chaining calls with promise : \n");

Clearly the nested structure goes deeper and deeper in a callback which makes it very difficult during its debugging.

However, the sequential dependent calls are all in consecutive .then() blocks in case of Promise. This looks more simpler and much closer to our favorite synchronous code.

 

2 : Chaining Multiple Promise Calls

In this example we are calling three dependent service back to back and, also, showing how to handle errors with a common .catch() block at the end.

Here each .then() is returning a separate promise which invokes its following .then() clause. In case-5, we will see what will happen if we associate multiple .then() to the same promise.

function addNextStep(previousStep, currStep, isSuccessful){
  return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        if(isSuccessful){
          resolve(previousStep+"==>"+currStep);
        } else{
          reject(previousStep+"==>Error at :"+currStep);
        }

      },1000);
  });
}

const promise = addNextStep("","service1",true)
.then((previousStep) => {
  console.log(previousStep);
  return addNextStep(previousStep,"service2",true);     //case-1
  //return addNextStep(previousStep,"service2",false);  //case-2
})
.then((previousStep) => {
  console.log(previousStep);
  return addNextStep(previousStep,"service3",true);
})
.then((previousStep) => {
  console.log(previousStep);
})
.catch((error) => {
  console.log("Error message :" + error);
});

/* 
Output for case-1 :
==>service1
==>service1==>service2
==>service1==>service2==>service3

Output for case-2 :
==>service1
Error message :==>service1==>Error at :service2
*/

case-2 shows how throwing an error at service-2 skips the following .then() and goes directly to .catch()

 

3 : Running Multiple Promise Calls Independently

These chaining mechanism also allows us to run multiple sequences in parallel without blocking the main thread.

function addNextStep(previousStep, currStep, isSuccessful){
  console.log("Executing :"+currStep);
  return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        if(isSuccessful){
          resolve(previousStep+"==>"+currStep);
        } else{
          reject(previousStep+"==>Error at :"+currStep);
        }

      },1000);
  });
}


const promise1 = addNextStep("","getSavingsAccountBalance",true)
.then((data) => {
  console.log(data+". Data received for user display.");
});
const promise2 = addNextStep("","getLatestTransactions",true)
.then((data) => {
  console.log(data  +". Data received for user display.");
});
const promise3 = addNextStep("","getFixedDepositeBalance",true)
.then((data) => {
  console.log(data+". Data received for user display.");
});

console.log("Continue with other transactions....");

/* 
Output :
Executing :getSavingsAccountBalance
Executing :getLatestTransactions
Executing :getFixedDepositeBalance
Continue with other transactions....
==>getSavingsAccountBalance. Data received for user display.
==>getLatestTransactions. Data received for user display.
==>getFixedDepositeBalance. Data received for user display.

*/

 

4 : Diverging into Multiple Sequences
Two sequence starting from a single Promise

Here we will see how can we create multiple sequence from a common asynchronous call or a promise object.

Importantly, each of the consecutive .then() clause as in case-2 continues on the promise object of its previous .then(). Hence, that was a single sequence of dependent calls.

In contrast, here we are using separate .then() on the same promise object -promise1. Hence, each .then() starts a new sequence on the same promise or the asynchronous call as shown above.

function addNextStep(previousStep, currStep, isSuccessful){
  return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        if(isSuccessful){
          resolve(previousStep+"==>"+currStep);
        } else{
          reject(previousStep+"==>Error at :"+currStep);
        }

      },1000);
  });
}

const promise1 = addNextStep("","CustomerAccounts",true);

promise1
.then((data) => {
  console.log("Data Received :"+data);
});
//Data Received :==>CustomerAccounts

promise1
.then((data) => {
  return addNextStep(data,"SavingsAccountBalance",true);
})
.then((data) => {
  console.log("Data Received :"+data);
});
//Data Received :==>CustomerAccounts==>SavingsAccountBalance


promise1
.then((data) => {
  return addNextStep(data,"LatestSavingsAccountTransactions",true);
})
.then((data) => {
  console.log("Data Received :"+data);
});
//Data Received :==>CustomerAccounts==>LatestSavingsAccountTransactions

/* 
Output :
Data Received :==>CustomerAccounts
Data Received :==>CustomerAccounts==>SavingsAccountBalance
Data Received :==>CustomerAccounts==>LatestSavingsAccountTransactions
*/

 

5 : Converging Multiple Promise Calls
Converging parallel Promise calls

While using multiple promise calls in parallel, promise also allows us to wait for all the calls to complete before proceeding to the next step.

In the example below, we are waiting for promise2 and promise3, to complete using Promise.all.

function addNextStep(previousStep, currStep, isSuccessful){
  return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        if(isSuccessful){
          resolve(previousStep+"==>"+currStep);
        } else{
          reject(previousStep+"==>Error at :"+currStep);
        }

      },1000);
  });
}

const promise1 = addNextStep("","CustomerAccounts",true)
.then((data) => {
  console.log(data);
  var promise2 = addNextStep(data,"SavingsAccountBalance",true);
  var promise3 = addNextStep(data,"LatestSavingsAccountTransactions",true);
  return Promise.all([promise2,promise3]);
})
.then((dataList) => {
  console.log("dataList :");
  dataList.forEach(console.log);
});

/* 
Output :
==>CustomerAccounts
dataList:
==>CustomerAccounts==>SavingsAccountBalance
==>CustomerAccounts==>LatestSavingsAccountTransactions
*/

 

6 : Choose the fastest among the parallels

Complementary to the above example where we are waiting for all the parallel promise to complete; we also have the option to return only after one call completes.

  1. Promise.race : It returns after any one of the parallel promise returns. The success or failure does not matter.
  2. Promise.any : It returns after any one of the parallel promise returns successfully or all the promise complete.
const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, 'very slow'));
const promise2 = new Promise((resolve,reject) => setTimeout(reject, 200, 'failed, but fast'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow, but successful'));

const promises = [promise1, promise2, promise3];

Promise.race(promises)
.then((value) => console.log("Race :"+value))
.catch((value) => console.log("Race Error: "+value));
//Race Error: failed, but fast

Promise.any(promises)
.then((value) => console.log("Any :"+value))
.catch((value) => console.log("Any Error:"+value));
//Any :slow but, successful

 

Summary

The chaining helps us stitch our requests broken down into smaller parts in Nodejs programming. The promise provides a simpler syntax to build these call chains.

  • There is no complex nesting as in the callback, since each promise call has its own then.
  • We can handle errors at a single place at the end if we need a generic handling.
  • Promise makes it easy to build various kinds of parallel and sequential call chains. Building such complicated call chains using callbacks would be very complicated.

As we have seen in the event loop article, Nodejs registers the callbacks from the promise in the fast track queue, the JobQueue, outside of the event loop.