Jaa


Promises and Futures in JavaScript

[Note: A new version of Promise.js is available here. It has a less restrictive license and additional functionality. Please be sure to check it out. ]

A Promise is a programming model that deals with deferred results in concurrent programming.  The basic idea around promises are that rather than issuing a blocking call for a resource (IO, Network, etc.) you can immediately return a promise for a future value that will eventually be fulfilled.  This allows you to write non-blocking logic that executes asynchronously without having to write a lot of synchronization and plumbing code.

For example, lets say we want to load an RSS feed via HTTP.  To accomplish this in JavaScript we have to leverage the XMLHttpRequest object.  The following example shows how one might load the RSS feed from this blog using conventional means:

  1: function fetchSync(url) {
  2:   var xhr = new XMLHttpRequest();
  3:   xhr.open(url, "GET", false, null, null);
  4:   xhr.send(null);
  5:   if (xhr.status == 200) {
  6:     return xhr;
  7:   }
  8:   throw new Error(xhr.statusText);
  9: }
  10:  
  11: function fetchAsync(url, callback, errorCallback) {
  12:   var xhr = new XMLHttpRequest();
  13:   xhr.onreadystatechange = function() {
  14:     if (xhr.readyState == 4) {
  15:       if (xhr.status == 200) {
  16:         callback(xhr);
  17:       }
  18:       else {
  19:         errorCallback(new Error(xhr.statusText));
  20:       }
  21:     }
  22:   }
  23:   xhr.open(url, "GET", true, null, null);
  24:   xhr.send(null);
  25: }

 

In the first example, the RSS feed is fetched synchronously (see line 3).  In this case the code is straightforward but the request is blocking, which means script execution is suspended until the request is completed or fails.  This is not optimal in an AJAX application as it can impact the interactivity of the page.

The second example attempts to alleviate the blocking call by introducing a callback that will be executed when the request completes.  This is advantageous as it is non-blocking, however the programming model can become inconsistent if you are working with different asynchronous data sources including loading local resources, performing computationally intensive tasks (such as xml transforms or other data transformation), etc.

To actually use the resulting feed you might have to do some additional work:

  1: fetchAsync(
  2:   "https://blogs.msdn.com/rbuckton/rss.xml", 
  3:   function (rss) {
  4:     fetchAsync(
  5:       "/some/stylesheet.xsl",
  6:        function (xsl) {
  7:          document.getElementById("host").innerHTML = rss.responseXML.transformNode(xsl.documentElement);
  8:        },
  9:        function (e) {
  10:          document.getElementById("host").innerHTML = "Error: " + ex.message;
  11:        })
  12:   },
  13:   function (ex) {
  14:     document.getElementById("host").innerHTML = "Error: " + ex.message;
  15:   });

In this example we now have two asynchronous calls for content, however one depends on the other and as a result we can’t take advantage of concurrent requests to speed up the process. 

Now lets look at a concurrent example:

  1: var rss = null;
  2: var xsl = null;
  3:  
  4: function tryComplete() {
  5:   if (rss != null && xsl != null) {
  6:     document.getElementById("host").innerHTML = rss.responseXML.transformNode(xsl.documentElement);
  7:   }
  8: }
  9:  
  10: fetchAsync(
  11:   "https://blogs.msdn.com/rbuckton/rss.xml",
  12:    function(rssXhr) {
  13:      rss = rssXhr;
  14:      tryComplete();
  15:    },
  16:    function (e) {
  17:      document.getElementById("host").innerHTML += "Error: " + e.message + "<br />";
  18:    });
  19:  
  20: fetchAsync(
  21:   "/some/stylesheet.xsl",
  22:   function (xslXhr) {
  23:     xsl = xslXhr;
  24:     tryComplete();
  25:   },
  26:    function (e) {
  27:      document.getElementById("host").innerHTML += "Error: " + e.message + "<br />";
  28:    });

Now the above example can utilize concurrent requests and synchronize the results, but wow that’s a lot code.

Using promises you can instead do the following:

  1: var prss = Promise.fetch("https://blogs.msdn.com/rbuckton/rss.xml");
  2: var pxsl = Promise.fetch("/some/stylesheet.xsl");
  3: Promise.whenOnly(
  4:   Promise.join(prss, pxsl),
  5:   function() {
  6:     var rss = prss.get_value();
  7:     var xsl = pxsl.get_value();
  8:     document.getElementById("host").innerHTML = rss.responseXML.transformNode(xsl.responseXML);
  9:   }, 
  10:   function(ex) {
  11:     document.getElementById("host").innerHTML = "Error: " + ex.message;
  12:   });

 

 

Another feature of Promises is pipelining, the ability to create a series of operations based on the result of promises.  The Promise.when() function is used to pipeline a promise, creating a new Promise based on the eventual output of the source.  For example:

  1: var pfeed = Promise.fetch("somefeed.xml");
  2:  
  3: var pfeedXml = Promise.when(pfeed, function(feed) { return feed.responseXML; });
  4: Promise.whenOnly(pfeedXml, function(feedXml) { alert(feedXml.xml); });
  5:  
  6: var pfeedText = Promise.when(pfeed, function(feed) { return feed.responseText; });
  7: Promise.whenOnly(pfeedText, function(feedText) { console.log(feedText); });

On line 1 we fetch an xml document from the server.  On line 3 we create a pipeline that will eventually convert the response into an XML document.  On line 4 when the document is finally available we can alert the user.  On line 6 we create a pipeline that will eventually get the response value as text.  On line 7 we can log it to the console.

Here’s an example of using pipelined Promises to get the public twitter timeline in JSONP:

  1: Promise
  2:   .jsonp("https://twitter.com/statuses/public_timeline.json")
  3:   .whenOnly(function(timeline) {
  4:     alert(timeline.length);
  5:   });

 

A promise can be created for other deferred actions as well. For example:

  1: var pinput = new Promise(
  2:   function(fulfillPromise, breakPromise) {
  3:     var btn = document.getElementById("button");
  4:     var txt = document.getElementById("txt");
  5:     btn.onclick = function() {
  6:       fulfillPromise(txt.value);
  7:     }
  8:   }
  9: );
  10:  
  11: pinput.whenOnly(function(value) { alert(value); });

 

The above sample creates a promise for eventual input that you can act on at any point.

Whether a promise completes after a delay or very quickly (even synchronously), the programming model doesn’t change.  The Promise model can even handle present information, so that present and future data can be mixed within the model.  This is due to Promise.immediate() , which creates a promise that is fulfilled synchronously.  All of the “static” methods on the Promise object will implicitly create a Promise for any non-future data (data that is not already encapsulated in a Promise).

Here’s an example of a future factorial (based on the Wikipedia example):

  1: function factorial(value) {
  2:   return new Promise(
  3:     function(fulfillPromise, breakPromise) {
  4:       setTimeout(
  5:         function() {
  6:           var result = 0;
  7:           do {
  8:             result += value;
  9:           }
  10:           while (--value > 0);
  11:           fulfillPromise(result);
  12:         },
  13:         1);
  14:     }
  15:   );
  16: }
  17:  
  18: var pfactorial = factorial(100000)
  19:   .when(function(result) { return result + 3; })
  20:   .whenOnly(function(result) { alert(result); });

This example uses setTimeout (at line 4) to ensure the request returns immediately and does not block linear execution of the code.

*NOTE: The attached API does not overcome the Same Origin Policy when used in the browser.  This means that you can only make requests back to the same domain as the caller.  Same Origin Policy does not apply for Gadgets (Windows 7 and Windows Vista) or for Widgets (Windows Mobile 6.5), which is where I’ve used this library for the most part. I have added some JSONP support in the library however JSONP is very limited.

Here is a quick overview of the current Promise API:

 // constructor

var p = new Promise(initCallback) 
// initCallback: callback function that takes two 
// parameters, the fulfillPromise and breakPromise 
// callback functions.

// members

p.get_isComplete() 
// Gets whether the promise is complete
// returns: Boolean value that is True if the promise is 
// complete

p.get_value()
// Gets the value of the promise or 'undefined' if the 
// promise has not completed. If the promise was 
// broken during execution an exception will be thrown
// returns: Object value 

p.call(name, params)
// Calls a named function on the future value of the 
// promise
// name: String value for name of the field on the 
// promise result object that contains a function 
// to call
// params: parameter array of arguments to the 
// function. specified as in-line arguments not as an
// array: p.call("foo", 1, 2, 3)
// returns: a promise for the result of the call

p.getValue(name)
// Gets the value of a named field on the future
// value of the promise
// name: String value for the name of a field on the
// promise result object
// returns: a promise for the value of the field

p.setValue(name, value)
// Sets the value of a named field on the future
// value of the promise
// name: String value for the name of a field on the
// promise result object
// value: Object value to set on the field for the
// promise result object
// returns: none

p.when(fulfillPromise, breakPromise, context)
// Creates a promise for the value of an action taken
// on the future value of the promise
// fulfillPromise: Function that takes the value of the
// promise when it completes and is used to act
// on the future value. The result is used as the 
// value of the new promise
// breakPromise: Function that takes the exception
// raised by the promise when it fails and is used
// to act on the failure. The result is used as the
// value of the new promise
// context: Optional Object to use as the 'this' value
// for the fulfillPromise or breakPromise callbacks
// returns: a Promise for the result of actions taken
// in fulfillPromise or breakPromise

p.whenOnly(fulfillPromise, breakPromise, context)
// Enqueues an action to be taken on the future 
// value of the promise
// fulfillPromise: Function that takes the value of the
// promise when it completes and is used to act
// on the future value. 
// breakPromise: Function that takes the exception
// raised by the promise when it fails and is used
// to act on the failure. 
// context: Optional object to use as the 'this' value
// for the fulfillPromise or breakPromise callbacks
// returns: none

p.join(promises)
// Creates a new promise that completes when all
// joined promises have completed
// promises: Parameter array of other promises
// returns: a new Promise


// static/type members

Promise.when(promise, fulfillPromise, breakPromise, context)
// Creates a promise for the value of an action taken
// on the future value of the promise
// promise: The promise on which to act
// fulfillPromise: Function that takes the value of the
// promise when it completes and is used to act
// on the future value. The result is used as the 
// value of the new promise
// breakPromise: Function that takes the exception
// raised by the promise when it fails and is used
// to act on the failure. The result is used as the
// value of the new promise
// context: Optional Object to use as the 'this' value
// for the fulfillPromise or breakPromise callbacks
// returns: a Promise for the result of actions taken
// in fulfillPromise or breakPromise

Promise.whenOnly(promise, fulfillPromise, breakPromise, context)
// Enqueues an action to be taken on the future 
// value of the promise
// promise: The promise on which to act
// fulfillPromise: Function that takes the value of the
// promise when it completes and is used to act
// on the future value. 
// breakPromise: Function that takes the exception
// raised by the promise when it fails and is used
// to act on the failure. 
// context: Optional object to use as the 'this' value
// for the fulfillPromise or breakPromise callbacks
// returns: none

Promise.join(promises)
// Creates a new promise that completes when all
// joined promises have completed
// promises: Parameter array of other promises
// returns: a new Promise

Promise.make(value)
// Makes a promise on a value. If the value is a promise it 
// is returned. If the value is not a promise an immediate
// promise is returned
// value: an Object that may or may not be a promise
// returns: a Promise for the value

Promise.broken(exception)
// Makes a promise that is immediately broken
// exception: The error for the promise
// returns: a broken Promise for the exception

Promise.immediate(value)
// Makes a promise that is immediately fulfilled
// value: The value for the promise
// returns: a fulfilled Promise for the value

Promise.fetch(url, method, username, password, headers, query, body)
// Creates a Promise for a completed XMLHttpRequest for the 
// requested resource
// url: String value for the url
// method: Optional string value for the method. default is "GET"
// username: Optional string value for the username for Basic 
// Authentication. default is 'null'
// password: Optional string value for the password for Basic
// Authentication. default is 'null'
// headers: Optional Object containing name/value pairs for 
// HTTP headers. default is 'null'
// query: Optional Object or String containing name/value pairs
// to be added to the url as a querystring. default is 'null'
// body: Optional Object or String containing the request
// body for the request. A String is sent as-is while an Object
// contains name/value pairs that are url form encoded. 
// default is null
// returns: A Promise for the completed XMLHttpRequest

Promise.json(url, method, username, password, headers, query, body)
// Creates a Promise for a JSON object parsed from the result of an 
// XMLHttpRequest for the requested resource.
// This requires the JSON object which is Native in IE8. If you 
// wish to use this method on a browser that does not have the
// native JSON object, please download www.json.org/json2.js
// url: String value for the url
// method: Optional string value for the method. default is "GET"
// username: Optional string value for the username for Basic 
// Authentication. default is 'null'
// password: Optional string value for the password for Basic
// Authentication. default is 'null'
// headers: Optional Object containing name/value pairs for 
// HTTP headers. default is 'null'
// query: Optional Object or String containing name/value pairs
// to be added to the url as a querystring. default is 'null'
// body: Optional Object or String containing the request
// body for the request. A String is sent as-is while an Object
// contains name/value pairs that are url form encoded. 
// default is null
// returns: A Promise for the JSON value of the response

Promise.jsonp(url, query, callbackArg)
// Uses JSONP to request a JSON resource
// url: String value for the url
// query: Optional Object or String containing name/value pairs
// to be added to the url as a querystring. default is 'null'
// callbackArg: Optional string that specifies the name of
// a querystring argument used to hold the name of
// a callback function. default is "callback"

Promise.js is licensed under the MSR-LA for non-commercial use only. The license is included in license.txt in the zip.

promise.zip

Comments

  • Anonymous
    January 29, 2010
    Very cool. Thanks for sharing!

  • Anonymous
    March 19, 2010
    Really nice to see more examples of JS Promises used. I'd love to know of any sites that are currently leaning on their usage.

  • Anonymous
    March 21, 2010
    "I'd love to know of any sites that are currently leaning on their usage." That would require a more liberal license.

  • Anonymous
    July 05, 2010
    FuturesJS (github.com/.../futures) is available under the MIT license. It also handles subscriptions. I started a blog post explaining usage details: thesystemisntdown.blogspot.com/.../futuresjs-promises-subscriptions-joins.html

  • Anonymous
    September 15, 2011
    The comment has been removed

  • Anonymous
    September 16, 2011
    Thanks for catching that. I've fixed the example, but please refer to the updated post at blogs.msdn.com/.../promise-js-2-0-promise-framework-for-javascript.aspx

  • Anonymous
    October 19, 2011
    The comment has been removed