Either writing Javascript or NodeJS, we are always dealing with an asynchronous approach. We have to be very careful while implementing in this environment and also chanining callbacks will make your code not readable at all. - Even for yourself :) - General saying about this is Pyramid of Doom or Callback Hell

Let me give you an example - a developers day;

Pyramid of Doom or Callback Hell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
wakeup(function(alarm, callback) {
haveBreakfast(function(eggs) {
gotoOffice(function(car) {
writeCode(function(scrum, meeting, task) {
haveLunch(function(whereToEat) {
writeMoreCode(function(moretasks) {
gobackHome(function(car) {
haveDinner(function(isItReady) {
watchTV(function(phew) {
return calback('done with the day'));
})
})
})
})
})
})
})
})
})

I know that this is an exaggerated example, but I see codes nearly like this. This is when your code runs to the right faster than it runs forward. Let me give you a real world example;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
router.get('/subscribe', checkAuthentication, function (req, res) {
req.app.locals.title = 'Subscribtion';
req.app.locals.activePage = 'subscribe';
btManager.generateClientToken(function (err, clientToken) {
if (err) {
log.error(new Error(err));
res.render('subscribe', { errors: [{ msg: err }] });
return;
}
if (req.app.locals.user.subscribed) {
btManager.getPaymentMethods(req.app.locals.user.email, function (err, paymentMethods) {
if (err) {
log.error(new Error(err));
res.render('subscribe', { errors: [{ msg: err }], braintreetoken: clientToken });
return;
}
btManager.calculateSubscriptionPrice(req.app.locals.user.email, function (err, price) {
res.render('subscribe', {
braintreetoken: clientToken, paymentMethods: paymentMethods,
price: price
});
});
});
} else {
res.render('subscribe', { braintreetoken: clientToken });
}
});
});

Here this is a get method for checking for the existing subscription, if user subscribed already than gets his/her payment methods and also calculates the current subscription price on an express router. Even it looks not that complex, it can be much more readable. There are some ways to accomplish this. First and simple way is to give callback functions a name and not define them as anonymous functions. So let’s look at this approach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
router.get('/subscribe', checkAuthentication, function (req, res) {
req.app.locals.title = 'Subscribtion';
req.app.locals.activePage = 'subscribe';
btManager.generateClientToken(generateTokenCallback.bind({
req: req,
res: res
}));
});
function generateTokenCallback(err, clientToken) {
if (err) {
log.error(new Error(err));
this.res.render('subscribe', { errors: [{ msg: err }] });
return;
}
if (this.req.app.locals.user.subscribed) {
btManager.getPaymentMethods(this.req.app.locals.user.email,
getPaymentsCallback.bind({
req: this.req,
res: this.res,
clientToken: clientToken
}));
} else {
this.res.render('subscribe', { braintreetoken: clientToken });
}
}
function getPaymentsCallback(err, paymentMethods) {
if (err) {
log.error(new Error(err));
this.res.render('subscribe', { errors: [{ msg: err }], braintreetoken: this.clientToken });
return;
}
btManager.calculateSubscriptionPrice(this.req.app.locals.user.email,
calculateSubscriptionCallback.bind(
{
req: this.req,
res: this.res,
clientToken: this.clientToken,
paymentMethods: paymentMethods
}));
}
function calculateSubscriptionCallback(err, price) {
this.res.render('subscribe', {
braintreetoken: this.clientToken, paymentMethods: this.paymentMethods,
price: price
});
}

Hmm, so we did what we can to create named functions, but;

  1. What the hell? Now it looks even worse. Ok no Pyramid of Doom but…
  2. Our subscribe route only contains a call to generateClientToken. When someone see this, first impression is subscribe route only generates clientToken ??? Where is success render? – To realize this he needs to trace the callbacks.
  3. Just giving name to callbacks is not enough, because all callbacks are using either req or res and results from other callbacks inside, which are not defined for them. We need to pass this to the deep with bind. Looks realy awful.
  4. Harder to read or maintain, even we don’t have the Pyramid of Doom. Both of these are bad. A dilemma state :)

So our problem is not going right, but also tracing callbacks and passing all these variables and getting results from the callbacks. This is why Promises are used. There is a very good article about why we use promises in our codings. Check it out

Promises were around more than you thing, so it is not a javascript only approach.

Promise Version

Let’s change our router fire our promises. It will be much more readable and easy to maintain. We will have 3 promise for 3 async callback. See below codeblock for details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
req.app.locals.title = 'Subscribtion';
req.app.locals.activePage = 'subscribe';
var generateToken = new Promise(function (resolve, reject) {
btManager.generateClientToken(function (err, clientToken) {
if (err) {
reject(err);
} else {
resolve(clientToken);
}
});
});
var getPaymentMethods = new Promise(function (resolve, reject) {
btManager.getPaymentMethods(req.app.locals.user.email, function (err, paymentMethods) {
if (err) {
reject(err);
} else {
resolve(paymentMethods);
}
});
});
var calculateSubscriptionPrice = new Promise(function (resolve, reject) {
btManager.calculateSubscriptionPrice(req.app.locals.user.email, function (err, price) {
if (err) {
reject(err);
} else {
resolve(price);
}
});
});
// Pyramid for Promise chaining. Not a big one though :)
generateToken.then(function (clientToken) {
getPaymentMethods.then(function (paymentMethods) {
calculateSubscriptionPrice.then(function (price) {
res.render('subscribe', {
braintreetoken: clientToken, paymentMethods: paymentMethods,
price: price
});
}).catch(function (reason) {
log.error(new Error(reason));
res.render('subscribe', { errors: [{ msg: reason }], braintreetoken: clientToken });
});
});
}).catch(function (reason) {
log.error(new Error(reason));
res.render('subscribe', { errors: [{ msg: reason }] });
});

Promise Version 2

Looks better but still can be even better, because there is a small pyramid there which we don’t like :). So how to remove that pyramid. We would like to resolve those promises in order.

Promises can pass another promise to be resolved by others. So we can chain our promises in a better way and also we can collect the promise results in another object.
This is called flattening promise chain. Enough talking, let’s see how;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// Promise definitions are same as Version 1, so I omit them in this code.
/// Just an update for the small pyramid.
var scope = {};
Promise.resolve(generateToken)
// No more pyramids :)
.then(function (token) {
scope.clientToken = token;
return getPaymentMethods;
})
.then(function (paymentMethods) {
scope.paymentMethods = paymentMethods;
return calculateSubscriptionPrice;
})
.then(function (price, dfd, dfd, d) {
res.render('subscribe', {
braintreetoken: scope.clientToken,
paymentMethods: scope.paymentMethods,
price: price
});
}).catch(function (reason) {
log.error(new Error(reason));
res.render('subscribe', { errors: [{ msg: reason }] });
});

I believe we can do even better, since in node style all our callbacks are in form of callback(err, value). We can refactor the generation of promises. But hey, I cannot be the only one who can think of this :)

When you checkout the npm community, you come into bluebird, which is the best of promises I think. This package is dependend by anyone. Just go to www.npmjs.com and they are on the landing page under most depended-upon packages.

So what if we use bluebird for our example. Usage is simple. Put the below at the top of your js file;

1
var Promise = require("bluebird");

and see what happens to our whole code. No promise definition needed even. Bluebird will handle it for us. Checkout our last Version 3 below;

Promise Version 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
req.app.locals.title = 'Subscribtion';
req.app.locals.activePage = 'subscribe';
// Create promise version of all our methods to a promise.
// We don't need to define our promises.
btManager = Promise.promisifyAll(btManager);
// Join all our callbacks.
// No need to use scope variable to access our results.
Promise.join(
// Our methods are still there,
// But by default bluebird created the
// <methodname>Async versions of them
// Step 1
btManager.generateClientTokenAsync(),
// Step 2
btManager.getPaymentMethodsAsync(req.app.locals.user.email),
// And Step 3
btManager.calculateSubscriptionPriceAsync(req.app.locals.user.email),
// this our last then statement.
// All values returned by our callbacks are in order here.
// Wooww flattening for chain :)
function (clientToken, paymentMethods, price) {
res.render('subscribe', {
braintreetoken: clientToken,
paymentMethods: paymentMethods,
price: price
});
}).catch(function (reason) {
log.error(new Error(reason));
res.render('subscribe', { errors: [{ msg: reason }] });
});

Now our code is realy nice. Promises are realy fun and I hope you enjoyed reading. Waiting for your comments.

Thanks. Again here is the code for this block in github;

github Source Code