Promise Objects in JavaScript

2013-10-07 21:48

JavaScript’s default mode of operation is to rely heavily on callbacks: functions invoked when a longer operation (such as network I/O) finishes and delivers result. This makes it asynchronous, which is a notably different programming style than using blocking operations contained within threads (or their equivalents, like goroutines in Go).

Callbacks have numerous problems, though, out of which the most severe one is probably the phenomenon of “marching to the right”:

  1. doSomething(withTheseArgs, function(result) {
  2.     theDoSomethingElse(function() {
  3.         nowDoThis(toThat, function(result) {
  4.             andThen(function() {
  5.                 stop(hammertime, function(result) {
  6.                     // ...and so on...
  7.                 });
  8.             });
  9.         });
  10.     });
  11. });

When using the (still) common style of providing a callback as the last argument to a function initiating an asynchronous operation, you get this annoying result of ever-increasing indentation as you chain those operations together. It feels like the language itself is telling you that it was not designed for such a complex stuff… Coincidence? ;)

But it gets worse. Operations may fail somewhere along the way, which is something you’d probably like to know about. Depending on the conventions your project, framework or environment uses, this could mean additional boilerplate inside the callbacks to distinguish success from error cases. This is typical in Node.js, where first argument of callback represents the error, if any:

  1. fs.readFile('/etc/passwd', 'utf8', function (err,data) {
  2.   if (err) {
  3.     return console.log("Sorry, no hacking for you (" + err.message + ")");
  4.   }
  5.   console.log(data);
  6. });

Alternatively, you may be asked to provide the error handler separately; an “errback”, as it’s sometimes called. Splitting the code into small parts is great and everything, but here it means you’ll have two functions as arguments:

  1. doSomething(withThis, function(result) {
  2.     // ...
  3. }, function(error) {
  4.     // ...
  5. });

Giving them names and extracting somewhere outside may help readability a little, but will also prevent you from taking advantage of one of the JavaScript’s biggest benefits: superior support for anonymous functions and closures.

Promising solution

Thankfully, all this uphill struggle is not merely the language’s fault. What’s really flawed here is any API that blends data for long-running operations with callbacks that process their results. These two aspects should be kept separate, especially because the latter can be handled so much better.

Meet promises. Promise is an object that encapsulates the status of a time-consuming operation and allows callbacks to be attached to it. It may not sound very impressive (or even appear as unnecessary abstraction), but the advantages go way beyond simply making the code more clear.

Let’s start with that, though. There are few libraries implementing promises in JavaScript, and chances are that you’re already using one of them. Since quite some time now, jQuery has a very nice implementation of this concept, so if you happen to already include with your web application, I can wholeheartedly recommend having a look at it.
All examples here are using jQuery, but the ideas (or even API) are quite similar across the board.

Here’s a the first one, which should be both simple and useful:

  1. /**
  2.  * Loads an image from given path.
  3.  * @return jQuery Promise object
  4.  */
  5. function loadImage(path) {
  6.     var dfd = new $.Deferred();
  7.    
  8.     var image = new Image();
  9.     image.onload = function() { dfd.resolve(image); };
  10.     image.onerror = function() { dfd.reject(image); };
  11.     image.src = path;
  12.  
  13.     return dfd.promise();
  14. }

Loading an image, especially over the network, is surely a task best done asynchronously. But there is no explicit callback argument here, so how can we get the result once download completes?
No worries. This is exactly what the promise object is for, and we get it as a result from the above function. Rather than passing callback as the second argument, we can now attach it to the promise:

  1. loadImage('http://placekitten.com/200/300').done(function(kitten) {
  2.     var ctx = $('canvas').get(0).getContext('2d');
  3.     ctx.drawImage(kitten, 0, 0);
  4. });

Similarly, we can specify an error handler for it:

  1. loadImage('http://www.example.com/does/not/exist').fail(function() {
  2.     console.log("Your URL was bad and you should feel bad.");
  3. });

or even both callback and “errback”, all while remaining neat and readable:

  1. // Detecting if user is logged in to Twitter
  2. // (see http://www.tomanthony.co.uk/blog/detect-visitor-social-networks/ for explanation)
  3. loadImage('https://twitter.com/login?redirect_after_login=%2Fimages%2Fspinner.gif')
  4.     .done(function() { console.log("Logged in"); })
  5.     .fail(function() { console.log("Not logged in"); })

How to fulfill a promise

This is, in a nutshell, probably the simplest possible use of jQuery’s Promise. If you look into the loadImage function, however, you will notice that we’re mostly operating on a different type of entity: the $.Deferred object.

Why? $.Deferred is the jQuery-specific Deferred object, which acts as a “controller” for promise. Unlike the jQuery.Promise we return from our function, the Deferred object allows to change the state of the operation it “wraps”. It can be resolved – which indicates success – or it can be rejected – which signifies failure. We use these two methods in onload and onerror handlers of Image, thus “translating” them into the done/fail callbacks on a Promise.

Moreover, Deferred also implements all the regular API of Promise. This is quite powerful feature that allows to mix additional logic into your asynchronous pipelines.
For example, let’s take an AJAX request; it may fail for network connectivity reasons, but you could also have your server respond with an error, and that would technically still be counted as success. We can fix that by intercepting the AJAX response early and rejecting the Deferred object:

  1. /**
  2.  * Example function wrapping some AJAX request.
  3.  */
  4. function getNewItems(since) {
  5.     return $.getJSON('/ajax/feed', {since: since}).done(function(data, _, jqXHR) {
  6.         if (data.error) {  // server-specific error condition
  7.             jqXHR.reject(data.error);
  8.         }
  9.     });
  10. }

Callers of above function can chain more done callbacks, but those will only be invoked if JSON returned in the response does not contain the "error" key. Otherwise, the fail ones will get called instead, by the virtue of Deferred object (here, jqXHR) being rejected.

Combining them

The last important advantage of promises I wanted to discuss here is their composability. What would be quite convoluted with vanilla callbacks, it’s very easy with promises. We can tie few Promise objects together, creating a compound one that represents the joint state of all of them. This way it’s possible to, for example, launch a few AJAX requests concurrently and wait for them all to finish.

In jQuery, this can be achieved through the use of $.when function. Simply pass a few existing Promises to it and you will get a single one that encompasses them all:

  1. $.when(
  2.     loadImage('/img/kitten1.jpg'),
  3.     loadImage('/img/kitten2.jpg'),
  4.     loadImage('/img/kitten3.jpg'))
  5. .done(function() {
  6.     console.log("All cats loaded.");
  7. });

The resulting Promise behaves in rather predictable way: done callbacks are invoked when all “inner” promises are resolved, while fail gets called when any of them is rejected. For more fine grained control, you can also attach a then callback; there you’ll be able to evaluate results of each individual promise.

Okay, but what if we don’t want to hardcode all these loadImage calls, especially if we don’t even know how many of them we’ll have? This is not something supported specifically by jQuery, but JavaScript itself allows to call any function for arguments given as an array:

  1. /**
  2.  * Loads all images from given array of paths.
  3.  * @return jQuery Promise object
  4.  */
  5. function loadImages(paths) {
  6.     return $.when.apply($, paths.map(loadImage));
  7. }
  8.  
  9. // Usage:
  10. var IMAGES = [ /* some image paths */ ];
  11. loadImages(IMAGES).done(function() {
  12.     alert("Total of " + arguments.length + " images are loaded.");
  13. });

These examples should cover most of the use cases of promise objects. You may still want to use plain callbacks for the simplest of problems, but I’d recommend to at least have a try at doing it the, ahem, proper way :)

Be Sociable, Share!
Be Sociable, Share!
Tags: , , , ,
Author: Xion, posted under Programming »


3 comments for post “Promise Objects in JavaScript”.
  1. Tymur Porkuian:
    October 7th, 2013 o 22:06

    How about this?

    1. paths |> (map loadImage) |> combine ~> (\images -> alert "...")

    Any resemblance to existing languages is purely coincidental.

  2. D3vilroot:
    October 7th, 2013 o 22:27

    Nice article ;)
    This is a great promise object library https://github.com/kriskowal/q :)

  3. Xion:
    October 8th, 2013 o 11:52

    @up: Looks good. As I understand, though, you may still need to use jQuery for AJAX if you don’t want to wrap XMLHTTPRequest yourself (and you probably don’t). For sharing code between client side and Node.js backend, Q looks quite superb, however.

    @Tymur: Cute :) I think you could actually achieve something similar in JS if you add Underscore to the mix:

    1. _.chain(paths).map(loadImage).tap(_.bind($.when.apply, $)).value().done(function() { alert("...") });

    But personally, I’ll just… wouldn’t do that :)

Comments are disabled.
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.