Promises



Javascript uses two execution models for code, one is synchronous, meaning code is executed iteratively and effectively one line at a time. The other model is asynchronous, where a bit of code can be executed later, after waiting for some event to occur. The rest of the code will execute in a synchronous fasion until the asynchronous event happens.

Basics by Analogy

Promises can be confusing at first, but hopefully some analogies can help.

Imagine you’re a fireman in a bucket line, trying to put out a house fire. You’re given a bucket and you hand the bucket to the next person in line. This is equivalent to synchronous code.

Now, imagine being told by the captain to run to the fire house to get the fire engine. Cool, you always wanted to drive one, you promise you’ll be quick and the captain says “then we’ll help out”. You hop out of line and run to the fire house, and in the process the people in line move closer to fill in the gap you left behind. You return a little time later and now the other firemen come to help you set up the engine and get ready to put out the fire, they no longer need to work on the bucket line because you’re back with the fire truck. This is equivalent to asynchronous code.

A promise is provided in the second example by you telling the captain that you’ll be back. He takes your word for it and continues the previous work, but what happens if you get to the fire house and find out that the captain had the keys? You have to return empty-handed and tell the captain that there was a problem. When you get back with your problem the captain says “catch” as he throws you the keys.

In the end everything worked out for the better. The fire was put out and the work is complete. The captain looked around and, with a sigh of relief, said “finally“.

Before Promises

Back in the olden days, when the world was in black and white and the kids rode their pet dinosaurs to school. We used to develop asynchronous functions with callbacks.

function sayHello(callback){
    //mock an async call
    setTimeout(function(){
        var response = "Hello world!";
        callback(response);
    }, 5000);
}

sayHello(function(response){
    console.log(response);
});

This method tended to lead to “callback hell”, where you get your Dante’s Badge if you have to debug 7 layers of callbacks.

function sayHello(callback){
    //mock an async call
    setTimeout(function(){
        var response = "Hello world!";
        callback(response);
    }, 5000);
}
function sayItLouder(previous, callback){
    //mock an async call
    setTimeout(function(){
        var response = previous.toUpperCase();
        callback(response);
    }, 5000);
}
function evenLouder(previous, callback){
    //mock an async call
    setTimeout(function(){
        var response = previous+"!!!!!!!111!1";
        callback(response);
    }, 5000);
}

sayHello(function(response){
    sayItLouder(response, function(response2){
        evenLouder(response2, function(response3){
            console.log(response3);
        });
    });
});

JQuery Promises

Remember when we used JQuery to make AJAX requests? JQuery was one of the pioneers of using promise-like functions returned from asynchronous methods. It was slightly different than today’s version however.

$.get('/api/users', function(response){
        console.log("first success");
})
    .done(function(response){
        console.log("second success");
    })
    .fail(function(err){
        console.log("error", err);
    })
    .always(function(){
        console.log("finished");
    });

JQuery decided to create a callback success parameter at first, but then in JQuery 1.5 (2011-1-31) they added the deferred object with .done, .fail, and .always functions. They chose those method names because developers and standards organizations hadn’t finalized the function names for promises yet.

Other Promise Libraries

After the success with JQuery’s deferred promises, around April 29, 2012, Kris Kowal (kriskowal) created a Promise library called Q (in Angular this is $q). Instead of .done, it implemented .then which accepted a callback function parameter and an optional error function.

Some people might recognize the code below.

function sayHello(){
    var deferred = q.defer();

    setTimeout(function(){
        if(new Date().getTime() % 5 === 0){
            //resolves successfully on 0 or 5 in the last number of getTime()
            deferred.resolve("Hello world!");
        }else{
            deferred.reject("Not this time!");
        }
    }, 5000);

    return defrerred.promise;
}

sayHello().then(function(response){
    //Hello world!
    console.log(response);
}, function(err){
    //not this time
    console.log(err);
});

After some iterations, some more methods were added to Q. Now we don’t have to use the second then() parameter for errors, instead we get to catch() errors, just like a try/catch. We can also always execute a function, just like the .always() method in JQuery using Q’s .finally() method.

function sayHello(){
    var deferred = q.defer();

    setTimeout(function(){
        if(new Date().getTime() % 5 === 0){
            //resolves successfully on 0 or 5 in the last number of getTime()
            deferred.resolve("Hello world!");
        }else{
            deferred.reject("Not this time!");
        }
    }, 5000);

    return defrerred.promise;
}

sayHello().then(function(response){
    //Hello world!
    console.log(response);
}).catch(function(err){
    //not this time
    console.log(err);
}).finally(function(){
    console.log("finally")
});

Angular’s $http service provides Promise functionality that mimics both JQuery and Q. It includes .success(), .error(), .then(), .catch(), and .finally(). The Angular developers might have done this to allow JQuery developers to easily transition to using Angular without changing their coding conventions.

The .success() and .error() methods are aliases for .then() and .catch() respectively, however they deconstruct the HTTP response object and place the data as the first parameter of the success callback and include the status, headers, and config as additional parameters. This creates a tight coupling between the callee and the HTTP network. It makes it more difficult to change your backend to something different, such as WebSockets or client-side datastores.

Aviv Ben-Yosef provides a good reason to avoid using .success and .error in his post “Don’t use $http’s .success()“.

When using Angular’s $http service, only use the .then(), .catch(), and .finally() methods.

A Standardized Promise

The Promise constructor was added to the ECMAScript 2015 (ecma-262) specification in on January 20, 2014, Draft Rev 22. Even though it was standardized, it doesn’t mean it’s available throughout the various browsers. Client-side applications still need to target the browsers they want to support and include polyfills for missing features, which is where libraries like Q or Promise Polyfill come in.

ES6 Promises will work in Chrome 45, IE Edge, Firefox 46, Safari 9.1, Opera 38, iOS Safari 9.3 Android 4.4.4, and Chrome for Android 51.

http://caniuse.com/#feat=promises

Promises also work in PhantomJS and NodeJS, so if you’re creating server-side code using NodeJS then it’s safe to use ES6 Promises instead of Q.

You might notice that the promise created below returns similar methods as the Q library (.then, .catch, .finally), however the way that it’s instantiated is slightly different.

function sayHello(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if(new Date().getTime() % 5 === 0){
                //resolves successfully on 0 or 5 in the last number of getTime()
                resolve("Hello world!");
            }else{
                reject("Not this time!");
            }
        }, 5000);
    });
}

sayHello().then(function(response){
    //Hello world!
    console.log(response);
}).catch(function(err){
    //not this time
    console.log(err);
}).finally(function(){
    console.log("finally")
});

Chaining Promises

A neat feature of Promises is that the .then() method returns another promise. Chaining is a method of adding multiple .then statements that execute iteratively. Each .then’s parameter will be the response from the previous promise.

One common misconception is that you need to perform multiple asynchronous calls within the callback function for the .then, but this will lead you back into callback hell. Instead, we can flatten the promise and chain .thens together.

Instead of using a timeout for the examples below, I’m going to use an Angular $http call. We should already be aware that this method will return a promise.

Wrong

//a chat program
function getUsers(room){
    return $http.get('/api/users');
}
function getMessages(room){
    return $http.get('/api/messages');
}
function getRooms(){
    return $http.get('/api/messages');
}

$scope.messages = [];
$scope.rooms = [];
$scope.selectedRoom = {};
$scope.users = [];

getRooms().then(function(rooms){
    $scope.rooms = rooms;
    $scope.selectedRoom = rooms[0]
    getUsers($scope.selectedRoom).then(function(users){
        $scope.users = users;
        //must do this one after getUsers because we need to 
        //link the user to the message
        getMessages($scope.selectedRoom).then(function(messages){
            $scope.messages = messages;
            //link messages to users in the UI
        });
    });
});

Sometimes people forget that the $http service returns a promise and think that they need to create a deferred promise using the $q library for an $http call.

Xzibit

Wrong

//a chat program
function getUsers(room){
    return $http.get('/api/users');
}
function getMessages(room){
    return $http.get('/api/messages');
}
function getRooms(){
    return $http.get('/api/messages');
}

$scope.messages = [];
$scope.rooms = [];
$scope.selectedRoom = {};
$scope.users = [];

function initAndGetMessages(){
    //why?
    var deferred = $q.defer();

    getRooms().then(function(rooms){
        $scope.rooms = rooms;
        $scope.selectedRoom = rooms[0]
        getUsers($scope.selectedRoom).then(function(users){
            $scope.users = users;
            //must do this one after getUsers because we need to 
            //link the user to the message
            getMessages($scope.selectedRoom).then(function(messages){
                $scope.messages = messages;
                // ಠ_ಠ
                deferred.resolve(messages);
                //link messages to users in the UI
            });
        });
    });   
    //why? 
    return deferred.promise;
}

//I want the messages in this function too!
initAndGetMessages().then(function(messages){
    console.log("done!", messages);
});

Right

//a chat program
function getUsers(room){
    return $http.get('/api/rooms/'+room.code+'/users');
}
function getMessages(room){
    return $http.get('/api/rooms'+room.code+'/messages');
}
function getRooms(){
    return $http.get('/api/rooms');
}

$scope.messages = [];
$scope.rooms = [];
$scope.selectedRoom = {};
$scope.users = [];

getRooms().then(function(rooms){
    $scope.rooms = rooms;
    $scope.selectedRoom = rooms[0]
    return $scope.selectedRoom;
}).then(function(room){
    return getUsers(room).then(function(users){
        $scope.users = users;
        return room;
    }).catch(function(err){
        //can catch an idividual HTTP failure here, or all of them below.
        console.log("failed to get the users");
    });
}).then(function(room){
    //must do this one after getUsers because we need to 
    //link the user to the message
    return getMessages(room).then(function(messages){
        $scope.messages = messages;
        //link messages to users in the UI
        return messages;
    });
}).catch(function(err){
    //catches any HTTP error from any part of the code
    console.log("error", err);
}).finally(function(){
    //executes after everything is done
    console.log("finally");
});

Deferred Execution Until all Promises are Resolved — Promise.all

One last feature that needs mention is Promise.all(). It allows you to defer the execution of a function until a number of promises have resolved successfully.

While this is a very useful feature, in my opinion people tend to misuse it too much. It can block execution and prevent users from seeing valuable information on a page. If necessary, this function can be a powerful tool, but with great power comes great responsibility, and you should be aware that the function being deferred may not execute for a number of seconds or it might not execute at all if there was an error in just one of the promises.

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise(function(resolve, reject){
    setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(function(values){ 
    console.log(values); // [3, 1337, "foo"] 
});

Promise.all (also q.all or $q.all in Angular) includes a “fast-fail” method where you can catch an error in any of the promises and stop execution of the other ongoing promises.

var p1 = $http.get('/api/users/all');
var p2 = $http.get('/api/rooms');
var p3 = $http.get('/api/messages/all');
var p4 = $http.get('/badapi/not/gonna/work');

$q.all([p1, p2, p3, p4]).then(function(value){ 
  console.log(value);
}, function(err){ //.catch may also work.
  console.log("error", err);
});

Leave a Reply

Your email address will not be published. Required fields are marked *