Promises, Promises

An introduction to Deferred objects in Javascript


Richard Bateman


Who am I?


  • Lead Software Architect at GradeCam

Presentation goals

What do we want to accomplish during our time here?

  • Slow presentations bore me -- we're going to be moving right along.
  • Questions are welcome -- if I'm not being clear, please let me know.

    • We want to learn:
      • What are Promises?
      • What do they give us?
      • How do we use them?
      • What libraries are available, and what is the future?

    Javascript is asynchronous!          

    • Client-side javascript is often asynchronous
      • Ever-ubiquitous AJAX / JSONP
      • Web sockets
      • New HTML5 APIs, such as FileReader
      • Web workers
      • User interactions (e.g. modal dialog results)

    • Server-side javascript (e.g. node.js) is nearly always asynchronous
      • Disk access
      • Network access
      • Database access
      • etc

    Asynchronous code is ugly

    function getUserInfo(id, callback) {
        $.ajax({
            url: "/user/" + id,
            type: 'GET',
            dataType: 'html',
            success: function(data, textStatus, xhr) {
                callback(null, data);
            },
            error: function(xhr, textStatus, errorThrown) {
                callback(new Error(textStatus), errorThrown);
            }
        });
    }
    function getUserComments(id, callback) {
        $.ajax({
            url: "/user/" + id + "/comments",
            type: 'GET',
            dataType: 'html',
            success: function(data, textStatus, xhr) {
                callback(null, data);
            },
            error: function(xhr, textStatus, errorThrown) {
                callback(new Error(textStatus), errorThrown);
            }
        });
    }
    
    Here are a couple of standard AJAX functions.

    Nested functions are verbose

    // Get the user info
    getUserInfo(1, function(err, userDoc) {
        if (!err) {
            // get the user comments
            getUserComments(1, function(err, commentArray) {
                if (!err) {
                    DisplayUserInfoWithComments(userDoc, commentArray);
                } else {
                    // Do something to handle errors
                }
            });
        } else {
            // Do something to handle errors
        }
    }

    Not pretty and not efficient; no reason we can't do both simultaneously!

    Let's try that

    Parallel calls are even more verbose

    // Get the user info
    var userDoc = null;
    var commentArray = null;
    var wasError = false
    getUserInfo(1, function(err, inDoc) {
        if (!err) {
            userDoc = inDoc;
            if (commentArray) {
                DisplayUserInfoWithComments(userDoc, commentArray);
            }
        } else {
            wasError = err;
            wasError.errThrown = arguments[1];
        }
    });
    getUserComments(1, function(err, inArray) {
        if (!err) {
            commentArray = inArray;
            if (userDoc) {
                DisplayUserInfoWithComments(userDoc, commentArray);
            }
        } else {
            wasError = err;
            wasError.errThrown = arguments[1];
        }
    });
    ... and we thought the serial version was bad!

     Using jQuery Deferred

    // Deferred method
    function getUserInfo(id) {
        return $.ajax({
            url: "/user/" + id,
            type: 'GET',
            dataType: 'html',
        }).then(function(data, textStatus, xhr) { return data; });
    }
    function getUserComments(id, callback) {
        $.ajax({
            url: "/user/" + id + "/comments",
            type: 'GET',
            dataType: 'html'
        }).then(function(data, textStatus, xhr) { return data; });
    }

    jQuery $.ajax returns a Deferred object.
    Our then call discards the two extra parameters.

    Using the ajax calls with Deferred

    // Get the user info; deferred method
    var userDfd = getUserInfo(1);
    var commentsDfd = getUserComments(1);
    DisplayUserInfoWithComments(userDfd, commentsDfd);
    What, skeptical?
     
    Okay, it does need one more change:
    function DisplayUserInfoWithComments(userDfd, clistDfd) {
            $.when(userDfd, clistDfd).then(function(user, clist){
    // Previous contents of DisplayUserInfoWithComments in here
            }, function(err) {
    // Handle the error
            });
        }
        

    A promise is an asynchronous
    return value

    Traditional Callback:
    Call fn when you are done.
    Promise:
    Give me something I can use to get the result when I need it.

    Use the Promise like a variable until you actually need to use it, then handle all errors together!

    Two types of functions:

    • Operations that do something asynchronously and return a result

    • Operations that take data and do something with it



    Which of these should care about program flow?

    Separation of concerns

    Aspects of code:


    • Data access - Retrieves data from an outside source
    • Data manipulation - manipulates or changes the data
    • Presentation - Makes use of the data

    With promises, glue code need not know about the details of error handling and async operations!

    Case study


    Express REST API made easy with promises




    https://github.com/taxilian-promises/express-demo

    Multiple Async operations

    from bin/make_fake_data

    function cleanDatabase() {
        var waitFor = [];
    
        console.log("Cleaning database");
        waitFor.push(dropCollection('users'));
        waitFor.push(dropCollection('comments'));
    
        return Q.all(waitFor);
    }
    Most Promise libraries have something like Q.all -- jQuery has $.when, other libraries call it something different.  It accepts multiple Promises and resolves once all input Promises are resolve to an array containing the resolved value of each promise.

    If any of those Promises reject then the Promise returned by Q.all will immediately reject to the error that Promise rejected with.

    Sequential Asynchronous operations

    from bin/make_fake_data

    var doneDfd = Q(null); // Initial resolved promise
    
    doneDfd = doneDfd.then(cleanDatabase());
    
    // create users
    var userDfd, commentsDfd;
    for (var i = 0; i < 20; ++i) {
        // Note that since this is sequential, we wait on doneDfd
        userDfd = doneDfd.then(createRandomUser);
        commentsDfd = addCommentsForUser(userDfd);
    
        // The result Promise for this iteration is then stored back in doneDfd
        doneDfd = Q.all([userDfd, commentsDfd]);
    }
    
    doneDfd.then(function() {
        console.log("All done!");
    }, function(err) {
        console.error("Everything has broken with the error: ", err);
    });
    
    This is certainly not as clean as a synchronous language -- but can you imagine doing all of this sequentially using callbacks?

    Promise arguments

    from bin/make_fake_data

    function addCommentsForUser(userDfd) {
        // Before we can add the comments, we need userDfd to resolve first.
        // It is possible it may already be resolved, so we use Q.when which
        // will make it a promise -- if it's a value, the promise will be immediately
        // resolved.
        return Q.when(userDfd).then(function(user) {
            var savedObjects = [];
            var comment;
            for (var i = 0; i < 10; ++i) {
                comment = new Comment();
                comment.user = user;
                comment.text = "I " + randgen(verbs) + " " + randgen(first_names);
                savedObjects.push(Q.nbind(comment.save, comment)());
            }
            return Q.all(savedObjects);
        });
    }
    Note that userDfd (dfd is a prefix I use, short for "Deferred", another name for a Promise) might or might not be a Promise.  If it is a resolved User object this will work exactly the same.

    A promise is returned to indicate that all 10 comments were added.

    sendResponse

    an npm module that understands promises

    https://github.com/taxilian-promises/sendresponse

    sendResponse was adapted from a library we use at GradeCam

    This code comes from express-demo in routes/rest.js
    router.get('/users', function(req, res) {
        var userList = User.find().exec();
        res.sendResponse(userList);
    });
    
    router.get('/users/:email', function(req, res) {
        var user = getUserByEmail(req.params.email);
        res.sendResponse(user);
    });
    
    router.get('/users/:email/comments', function(req, res) {
        var userDfd = getUserByEmail(req.params.email);
        var comments = getCommentsForUser(userDfd);
    
        res.sendResponse(comments);
    });
    

    When making promises

    • Don't make a promise you don't intend to keep.

    • Trust, but verify!

    • For every promise, there is price to pay. ~Jim Rohn

    • Empty promises.

    • Instant Gratification

    Don't make a promise you don't intend to keep 

    A promise should eventually settle!
    // Wrong way!
    function getCoolStuff(someValue) {
        var deferred = Q.defer();
        setTimeout(function() {
            if (Something_cool_happened) {
                deferred.resolve(some_cool_value);
            }
        }, 500);
        return deferred.promise;
    }
    
    // Right way!
    function getCoolStuff(someValue) {
        var deferred = Q.defer();
        setTimeout(function() {
            if (Something_cool_happened) {
                deferred.resolve(some_cool_value);
            } else {
                deferred.reject(new Error("Nothing cool happened"));
            }
        }, 500);
        return deferred.promise;
    }
    

    Trust, but verify! 

    Always Always Always handle the error case!
    // Wrong way!
    GetDBRecord().then(function(result) {
        // Do something cool with the result
    });
    
    // Right way!
    GetDBRecord().then(function(result) {
        // Do something cool with the result
    }, function(err) {
        // Panic!
    });
    
    Always handling the error case does not mean you must always handle it right away -- if you return the Promise (or return the result of .then) then a calling method might handle it.

    A Promise whose reject case isn't handled will fail silently!

    For every promise, there is price to pay. ~Jim Rohn

    Be wary!
    • Excessive chaining causes excessive stack depth

    • If a promise exists, its resolution value exists, which can cause leaks!

    • Promises have relatively low overhead, but it can add up!

    Empty promises


    Sometimes a Promise is just used to mark success or failure

    function displayLoadingIndicator(promise) {
        showLoadingIndicator();
        var hide = function() { hideLoadingIndicator(); };
        Promise.when(promise).then(hide, hide);
    }
    

    Some libraries, such as jQuery Deferred, have a .always() shortcut for this

    Instant Gratification

    function changeTheUser(user) {
        return Promise.when(user).then(function(user) {
            // The user is resolved!
        });
    }
    
    When a function returns a promise, arguments can be a promise or a value. 

    The when function accepts a Promise or a value and return a Promise.

    If the input is not a Promise it immediately resolves to the value.

    ECMAScript 6 Promises

      • Still in development -- last updated in the draft April 3, 2015
      • Not final, but implemented in Chrome, Firefox, and node 0.12
      • Terminology: "fulfilled", "rejected", "pending".
        • "settled" means not pending.  "resolved" if it is either settled or "locked in" to match another promise.
      • Functions
        • Promise.all - accepts an array one or more Promises or values and  returns a promise that is fulfilled when all passed promises are fulfilled. Fulfills to an array of those promises or rejects to the reason of the first promise that is rejected.
        • Promise.race - accepts one or more values and returns a promise that is settled in the same way as the first promise to settle of the passed promises.
        • Promise.reject / Promise.resolve - Resolves or rejects the promise

    ECMAScript 6 Promises


    Different Syntax:

    function doSomethingAmazing(someValue) {
        return new Promise(function(resolve, reject) {
            CallAwesomeThing(someValue, function(err, result) {            if (err) { reject(err); }
                else { resolve(result); }
            });
        });
    }
    

    Promises/A+ Standard

    An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.

      • Most widely accepted standard for Javascript Promises
      • To fully support, must pass 872 unit test
      • At least partially supported by most Promise libraries
        • thennable just requires a then method
      • Three states: 'pending', 'fulfilled', 'rejected'
      • The then method must accept two arguments, onFulfilled and onRejected
      • Any Promise library supporting the Promises/A+ standard will be interoperable by nature of common support for the then method.


    The ECMAScript 6 is not Promises/A+ compliant, but is close.

    Example libraries: Q, Bluebird, ayepromise, and many others

    jQuery Deferred Objects

    Widely available on the client

    • Comes free with jQuery, already used by all jQuery async methods

    • jQuery 1.8 made Deferred objects thenable , so mostly (but not completely) interoperable with Promise/A+ libraries

    • Besides resolve and rejected states, supports an additional "progress" callback that can be triggered multiple times to give progress updates before it is settled.

    Promises, Promises 2015

    By Richard Bateman

    Promises, Promises 2015

    Slides / notes for my Promises, Promises talk at 2015 OpenWest (Updated from last year)

    • 3,382