JavaScript is a single threaded language which executes code asynchronously. For basic programming constructs, this does not make any difference, but for complex logic, the asynchronous code execution introduces extra complexity to the code. If you ever heard of Pyramid of Doom, you'll know what I am talking about. The JavaScript Jabber has a good podcast on async programming constructs and the Pyramid of Doom (Wikipedia) topic. A code example for Pyramid of Doom:
(function($) {
$(function(){
$("#btnUpdateDataAndUI").click(function(e) {
$.get("http://myawesomeapi.io/api/people", function(data) {
$(".rows").each(function() {
var that = this;
$(this).hover(function(e) {
setTimeout(function() {
$(that).fadeOut();
}, 1000);
});
});
});
});
});
})(jQuery);
What are Promises?
Using the promise constructs, developers can bypass the Pyramid of Doom problem. According to the Promise/A+ specification, a promise is the eventual result of an asynchronous operation. Eventually is the operational word here, because promises can succeed or fail. A promise has three mutually exclusive states: pending (the initial state), rejected (when the executed operation fails), and fulfilled (the executed operation was successfully completed). Promises are sometimes called thenable, and you'll see why later.
Show Me the Code
Below you can see a classic example of a promise:
service.getData()
.then(function (data) {
// do something with the
})
.fail(function (err) {
// handle error
});
The response of the service.getData is a promise for which we register a success route (the function passed to the then method) and a fail route (the function passed to the fail parameter). If the promise inside the getData() is fulfilled, the function in then method is executed. A diagram of the code can be seen below:
Concrete Examples
I present the q library, one of the many libraries which implement the promises specification. On this page you can find all the implementations of the Promise/A+ specification.
The q library can be used on the server-side (with node.js) or client side with any other framework. In the examples shown here, I used q on server side, inside an express Web application. The source code can be found in the jsexpense GitHub repository.
The q library can be installed with:
npm install q
Usage of then() and fail()
In the connectionManager.js file, I import the q library, along with the others that I need to use. This is needed for creating promises.
var mysql = require('mysql');
var q = require('q');
var LOG_PREFIX = '[CONN] - ';
function getConnection() {
var deferred = q.defer();
var connection = mysql.createConnection({
…
});
connection.connect(function (err) {
if (err) {
console.error(err);
deferred.reject(err);
}
console.log(LOG_PREFIX + 'Connection created with id:' + connection.threadId);
deferred.resolve(connection);
});
return deferred.promise;
}
Inside the getConnection function, I create a deferred object, which holds the interface through which I can reject or fulfill the promise. I then create and try to establish a connection using the connect function of MySQL connection (please read our guide for Using MySQL from Node.js if you have questions related to this code part). The connect method requires a callback method, which will be invoked once the connection is successfully created, or if it wasn't created due to an error (like MySQL server cannot be accessed).
The err parameter of the callback function helps decide the connection. If the err parameter does not have a truthy value then I log the error to the console and invoke the reject method of the deferred object. Invoking the reject method ensures that the framework will execute the function specified in the fail() method of the promise (as it can be seen on the state diagram above).
In case err object has a falsy value, then I invoke the resolve method of the deferred object and I pass the connection object to it as parameter. This will invoke the then() method of the promise and will pass the connection object as parameter to the function executed by the then method. At the end of the getConnection() function I return the promise of the deferred object. This makes it possible to use the fail() and then() methods after invoking getConnection, as seen in the example below:
connectionManager.getConnection()
.then(function (connection) {
// do something with the connection object
})
.fail(function (err) {
console.error(JSON.stringify(err));
});
Please notice that the then() and fail() methods are chained after the getConnection(). Both functions passed as parameters to the then() and fail(), received as parameter values that are passed to resolve() and reject() methods.
Another good example of using a promise is in api.js file:
router.get('/currencies', function(req, res) {
currencyManager.getCurrencies()
.then(function(data){
console.info(data);
res.json(data);
})
.fail(function(err){
res.status(500).json({error: err.message});
})
});
The router is an express.js router object used to define a new routing setup in the Web application. Inside the route handler I have the currencyManager.getCurrencies() method invoked, which returns a promise. In the success path (then() method), I log the data to the console and send it back as JSON data to the client. In case the getCurrencies() raised an error, the error object is returned with the error message and set with the HTTP response code 500 – Internal Server Error.
Chaining and Combining
Promises can be chained, for example:
router.get('/currencies', function(req, res) {
currencyManager.getCurrencies()
.then(function(data){
console.info(data);
return currencyManager.calculateConversionCost(
[{'EUR':1500}, {'USD':2130}],
data
);
})
.then(function(convertedValues){
console.log(convertedValues);
res.json({'exchanged': convertedValues});
})
.fail(function(err){
res.status(500).json({error: err.message});
})
});
In this code example, I have chained two then() methods one after another. The first then() writes the result of getCurrencies() to the console and returns the result of calculateConversionCost(). The result of this method is passed to the function in the second then() as convertedValues, where the values are logged and sent back as a JSON response to the client.
Chaining then methods is useful for when the order of the operations have to happen one after another.
Promises can also be combined. This is useful when the order of the operations can be mixed up. In the example below there are three promises.
router.get('/stats', function(req, res){
q.all([expenseManager.getExpenses(),
currencyManager.getCurrencies(),
memberManager.getMembers()
]).spread(function(expenses, currencies, members){
res.json({
expenses: expenses,
currencies: currencies,
members: members
});
});
});
I use the q.all() method, which should receive an array of promises to execute. It also uses the spread() method, which should have a callback function as parameter. The number of parameters in the callback function should match the number of promises passed to the q.all() method. In this example, I gave three promises to the q.all() method to execute, and I named three parameters in the callback function(expenses, currencies, members). It should be noted that the callback function will be called once all of the promises have settled.
The q library has other methods for handling multiple promises, like any(), which is similar to all() but it invokes the callback function after any of the promises have been fulfilled.
Promises are a great way to simplify asynchronous JavaScript code, but it has a learning curve that the developer has to adapt to. Once you get the idea, you will be able to fully utilize this technology in any JavaScript based application.