Nested Promise Resolution in JS : Avoiding ‘Call-Back Hell’

Promises are widely used in JavaScript for getting something in future. It has four possible states, namely:

  • fulfilled — The action relating to the promise succeeded
  • rejected — The action relating to the promise failed
  • pending — Hasn’t fulfilled or rejected yet
  • settled — Has fulfilled or rejected

This article is not meant for understanding promises, but rather for understanding how multiple promises could be chained for better readability and to avoid what is called ‘Call-back Hell’. The code snippets would be based on ECMAScript 5 and AngularJS, but could be easily used for other framework too.

More often than not, frontend application makes an http request to backend application to get the data which is displayed on the frontend which sometimes require some kind of transformations to be applied before rendering it on the view. It may also happen that we may need multiple http request’s to be made in a sequential order, i.e after getting data A, we need data B. Naive way would be to nest the .then of the promise.

I would give an example (both naive way and better way) which has the following requirement:

  • Make http request to endpoint ‘A’ and get the data.
  • Make transformation to data from ‘A’ and make http request to endpoint ‘B’ and using data from ‘A’.
  • Make transformation to data from ‘B’ and make http request to endpoint ‘C’ and using data from ‘B’.

Naive Way

getData('A')
.then(function (res) {
$scope.a = transformA(res);
getData('B')
.then(function (res) {
$scope.b = transformB(res, $scope.a);
getData('C')
.then(function(res){
$scope.c = transformC(res, $scope.b);
})
.catch(function (err) {
console.log(err);
});
.catch(function (err) {
console.log(err);
});
.catch(function (err) {
console.log(err);
});

Note:

  • getData() is method which makes http request to the url passed. For the purpose of example ‘A’, ‘B’ and ‘C’ have been used, in reality it takes string as an argument. getData() returns a promise which executes .then() block when fulfilled and .catch() block when rejected.
  • tranformA(), tranformB(), tranformC() are the transformation method. Notice that transformB() and transformC() are dependent on data from previous http calls.

Better Way

// Returns Promise
var resolverA = function (res){
$scope.a = transformA(res);
return getData('B');
};
// Returns Promise
var resolverB = function (res){
$scope.b = transformB(res, $scope.a);
return getData('C');
};
var resolverC = function (res){
$scope.c = transformA(res, $scope.b);
};
getData('A')
.then(resolverA)
.then(resolverB)
.then(resolverC)
.catch(function (err){
console.log(err);
});

Advantages Of Better Way

  • The code is much more readable.
  • Call-back hell avoided.
  • Single catch() to handle all the rejects.
The ratio of time spent reading (code) versus writing is well over 10 to 1 … (therefore) making it easy to read makes it easier to write.