Promise.js 2.0 – Promise Framework for JavaScript

Well over a year ago I posted an article describing how to employ an asynchrony concept known as a Promise.  Since then a number of implementations of Promises have shown up in libraries such as jQuery Deferred and CommonJS.  I’ve updated the sources and removed the dependency on ASP.NET Ajax, however the current version depends on features of ECMAScript 5, which is supported in IE 9, Chrome, and Firefox.  If you need support for Safari, Opera, or IE 8 and under you need to use an ES5 Polyfill.  In addition the license has been changed from MSR-LA (Non-Commercial) to MS-PL which allows commercial use.

Introduction

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.

License

Promise.js is licensed under the Microsoft Public License (MS-PL).

Source

promise.zip

Description

Promises are especially useful in JavaScript due to the fact that most of the JavaScript runtimes do not provide a means to block execution to wait on a result from a possibly long-running operation.  To facilitate rich interactive experiences in the browser, asynchrony is imperative.  All of the current generation (and many earlier generation) browsers support XMLHttpRequest as a means to asynchronously fetch content from the server on demand. Without a Promise model, fetching html from the server might use code like the following:

  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:   "/feeds/changes.rss",
  3:   function (rssXhr) {
  4:     fetchAsync(
  5:       "/resources/rss.xsl",
  6:       function (xslXhr) {
  7:         document.getElementById("host").innerHTML =
  8:           rssXhr.responseXML.transformNode(xslXhr.responseXML);
  9:       },
  10:       function (e) {
  11:         document.getElementById("host").innerHTML = 
  12:           "Error: " + e.message;
  13:       });
  14:   }, function (e) {
  15:     document.getElementById("host").innerHTML = 
  16:       "Error: " + e.message;
  17:   });

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.

Using promises we could instead do the following:

  1: var changes = Promise.get("/feeds/changes.rss");
  2: var xsl = Promise.get("/resources/rss.xsl");
  3: Promise
  4:   .all(changes, xsl)
  5:   .then(function(ar) {
  6:     document.getElementById("host").innerHTML = 
  7:       ar[0].responseXML.transformNode(ar[1].responseXML);
  8:   }, function(e) {
  9:      document.getElementById("host").innerText = e.message;
  10:   })

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

  1: Promise
  2:   .get("somefeed.xml")
  3:   .then({ 
  4:     resolve: function(xhr) { return xhr.responseXML }),
  5:     progress: function(xhr) { console.log("Loading...") }
  6:   })
  7:   .then({
  8:     resolve: function(xdoc) { console.log(xdoc.xml) },
  9:     reject: function(e) { console.error(e.message) )
  10:   })

On line 2 we fetch an xml document from the server. On line 4 we create a pipeline that will eventually convert the response into an XML document.  On line 8 when the document is finally available we can handle the results.

Here's an example using piplined promises and JSONP to get the public timeline from Twitter:

  1: Promise
  2:   .jsonp("https://api.twitter.com/1/statuses/public_timeline.json")
  3:   .then(function (data) {
  4:     data.forEach(function (tweet) {
  5:       var newTweet = createTweet(tweet);
  6:       document.getElementById("tweets").appendChild(newTweet);
  7:     });
  8:   })
  9:   .then({ reject: function (e) { alert(e) } });

Here's an example of computing a future factorial using Web Workers (supported in Safari, Chrome, Firefox, Opera and IE 10 (as of Platform Preview 2):

  1: // factorial.js
  2: onmessage = function(e) {
  3:   var value = e.data;
  4:   var result = 0;
  5:   do {
  6:     result += value;
  7:   }
  8:   while (--value > 0);
  9:   postMessage(result);
  10: }
 
  1: // client.js
  2: Promise
  3:   .work("factorial.js", 10)
  4:   .then(function(result) { console.log(result) })

Documentation

 // PromiseSource
var source = new PromiseSource()
/// <summary>Creates a new PromiseSource</summary>

source.isCanceled
/// <value type="Boolean">Gets a value indicating whether the Promise was 
/// canceled</value>

source.promise
/// <value type="Promise">
/// Gets the Promise for the current PromiseSource
/// </value>

source.resolve(value)
/// <summary>Resolve the promise using the provided value</summary>
/// <param name="value" type="Object" optional="true" mayBeNull="true">
/// The value for the promise
/// </param>

source.reject(error)
/// <summary>Reject the promise using the provided error</summary>
/// <param name="error" type="Error">The error for the promise</param>

source.progress(value)
/// <summary>Send a progress notification using the provided value</summary>
/// <param name="value" type="Object" optional="true" mayBeNull="true" />

var registration = source.cleanup(cleanupCallback)
/// <summary>
/// Register a callback used to cleanup when the related promise is canceled
/// </summary>
/// <param name="callback" type="Function">
/// The callback function to execute
/// </param>
/// <returns type="Function">
/// A function that can be used to unregister the cleanup callback
/// </returns>

source.cancel(dueTime)
/// <summary>
/// Cancels the promise and executes cleanup callbacks immediately or 
/// optionally after a period of time
/// </summary>
/// <param name="dueTime" type="Number" integer="true" optional="true">
/// Optional. The time in milliseconds to wait before canceling the promise
/// </param>


// Promise
var promise = new Promise(initCallback)
/// <summary>
/// Creates a new Promise for the provided completion callback
/// </summary>
/// <param name="init" type="Function">
/// A callback used to initialize the promise.
/// <signature>
/// <param name="source" type="PromiseSource">
/// The object used to resolve the promise
/// </param>
/// </signature>
/// </param>

promise.isComplete
/// <value type="Boolean">
/// Gets a value indicating whether the Promise has completed
/// </value>

promise.isFaulted
/// <value type="Boolean">
/// Gets a value indicating whether the Promise is faulted
/// </value>

promise.isCanceled
/// <value type="Boolean">
/// Gets a value indicating whether the Promise was canceled
/// </value>

promise.cancel(dueTime)
/// <summary>
/// Cancels the promise and executes cleanup callbacks immediately or 
/// optionally after a period of time
/// </summary>
/// <param name="dueTime" type="Number" integer="true" optional="true">
/// Optional. The time in milliseconds to wait before canceling the promise
/// </param>

var newPromise = promise.then(resolve, reject, progress)
var newPromise = promise.when(resolve, reject, progress)
/// <summary>
/// Creates a new promise that executes an action based on the value of the 
/// antecedent promise.
/// </summary>
/// <param name="resolve" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when the promise resolves successfully.
/// Its results are fed into a new Promise.
/// <signature>
/// <param name="value" type="Object">
/// The value from the antecedent promise
/// </param>
/// <returns type="Object">The result for the new promise</param>
/// </signature>
/// </param>
/// <param name="reject" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when the promise fails. Its results are 
/// fed into a new Promise.
/// <signature>
/// <param name="value" type="Object">
/// The error raised from the antecedent promise
/// </param>
/// <returns type="Object">The result for the new promise</param>
/// </signature>
/// </param>
/// <param name="progress" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when a promise posts progress.
/// <signature>
/// <param name="value" type="Object">
/// The progress value from the antecedent promise
/// </param>
/// </signature>
/// </param>
/// <returns type="Promise">
/// The promise created for the provided callbacks
/// </returns>

var newPromise = promise.then(options)
var newPromise = promise.when(options)
/// <summary>
/// Creates a new promise that executes an action based on the value of the 
/// antecedent promise. 
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "resolve", "reject", and "progress"
/// </param>
/// <returns type="Promise">
/// The promise created for the provided callbacks
/// </returns>

var newPromise = promise.get(name)
/// <summary>
/// Creates a promise for the value of the data property with the specified 
/// name from the result of this promise
/// </summary>
/// <param name="name" type="String">The name of the data property</param>
/// <returns type="Promise">
/// A new Promise that returns the value of the data property
/// </returns>

var newPromise = promise.put(name, value)
/// <summary>
/// Creates a promise that completes without a value after setting the value 
/// of the data property with the specified name from the result of this 
/// promise
/// </summary>
/// <param name="name" type="String">The name of the data property</param>
/// <param name="value" type="Object" mayBeNull="true">
/// The value for the data property
/// </param>
/// <returns type="Promise">
/// A new Promise with no value that completes after the value of the data 
/// property is set
/// </param>

var newPromise = promise.post(name, args)
/// <summary>
/// Creates a Promise that returns the result of calling the named function 
/// with the supplied arguments on the result of this promise. Arguments are 
/// passed in a fashion similar to Function.prototype.apply
/// </summary>
/// <param name="name" type="String">The name of the function</param>
/// <param name="args" type="Array">
/// An array of arguments for the function
/// </param>
/// <returns type="Promise">
/// A new Promise that returns the value of the function call
/// </returns>

var newPromise = promise.send(name, ...args)
/// <summary>
/// Creates a Promise that returns the result of calling the named function 
/// with the supplied arguments on the result of this promise. Arguments are 
/// passed in a fashion similar to Function.prototype.call
/// </summary>
/// <param name="name" type="String">The name of the function</param>
/// <param name="args" type="Object" paramArray="true">
/// A parameter array of arguments for the function
/// </param>
/// <returns type="Promise">
/// A new Promise that returns the value of the function call
/// </returns>

var newPromise = promise.unwrap()
/// <summary>
/// Creates a Promise for the underlying value of a Promise who's result is 
/// itself a Promise
/// </summary>
/// <result type="Promise">
/// A promise for the underlying value of the Promise
/// </result>

var newPromise = promise.end()
/// <summary>
/// Creates a Promise that throws any unhandled exception to the host object.
/// </summary>
/// <returns type="Promise" />

Promise.canUseWorker
/// <value static="true" type="Boolean">
/// Gets a value indicating whether Web Workers are supported
/// </value>

Promise.isPromise(value)
/// <summary>Determines whether the provided value is a Promise</summary>
/// <param name="promise" type="Object">
/// The object that may be a promise
/// </param>
/// <returns type="Boolean">
/// True if the value provided is a Promise, otherwise false
/// </returns>

var promise = Promise.resolve(value)
/// <summary>Returns the provided value as a Promise</summary>
/// <param name="value" type="Object" mayBeNull="true">
/// The value to Promise
/// </param>
/// <returns type="Promise">The Promise for the value</summary>
/// <remarks>
/// If the provided value is already a Promise it is returned. If it is a 
/// PromiseSource its associated Promise is returned. Otherwise, a new 
/// Promise is created and fully resolved with the provided value
/// </remarks>

var promise = Promise.reject(error)
/// <summary>
/// Creates a Promise that is faulted using the provided Error
/// <summary>
/// <param name="error" type="Error" />
/// <returns type="Promise">The faulted Promise</returns>

var promise = Promise.delay(dueTime, value)
/// <summary>
/// Creates a Promise that completes after a period of time using the 
/// optionally provided value
/// </summary>
/// <param name="dueTime" type="Number" integer="true">
/// The due time in milliseconds at which the Promise will complete
/// </param>
/// <param name="value" type="Object" optional="true" mayBeNull="true">
/// An optional value to use as the result of the Promise
/// </param>
/// <returns type="Promise" />

var promise = Promise.start(func, dueTime)
/// <summary>
/// Creates and schedules a Promise to execute the provided function at a 
/// later time
/// </summary>
/// <param name="func" type="Function">
/// The callback to execute whose return value will be the result of the 
/// Promise.
/// <signature>
/// <returns type="Object">The result for the Promise</returns>
/// </signature>
/// </param>
/// <param name="dueTime" type="Number" integer="true" optional="true">
/// The due time at which to start the callback. The default is immediately
/// </param>
/// <returns type="Promise" />

var promise = Promise.when(promise, resolve, reject, progress)
/// <summary>
/// Creates a new promise that executes an action based on the value of the 
/// antecedent promise.
/// </summary>
/// <param name="promise" type="Object">
/// The promise to monitor for a result. If the argument is not a Promise, 
/// one will be created for it
/// </param>
/// <param name="resolve" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when the promise resolves successfully. 
/// Its results are fed into a new Promise.
/// <signature>
/// <param name="value" type="Object">
/// The value from the antecedent promise
/// </param>
/// <returns type="Object">The result for the new promise</param>
/// </signature>
/// </param>
/// <param name="reject" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when the promise fails. Its results are 
/// fed into a new Promise.
/// <signature>
/// <param name="value" type="Object">
/// The error raised from the antecedent promise
/// </param>
/// <returns type="Object">The result for the new promise</param>
/// </signature>
/// </param>
/// <param name="progress" type="Function" optional="true" mayBeNull="true">
/// The callback that is executed when a promise posts progress.
/// <signature>
/// <param name="value" type="Object">
/// The progress value from the antecedent promise
/// </param>
/// </signature>
/// </param>
/// <returns type="Promise">
/// The promise created for the provided callbacks
/// </returns>

var promise = Promise.when(options)
/// <summary>
/// Creates a new promise that executes an action based on the value of the 
/// antecedent promise.
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "promise", "resolve", "reject", and 
/// "progress"
/// </param>
/// <returns type="Promise">
/// The promise created for the provided callbacks
/// </returns>

var promise = Promise.any(...promises)
/// <summary>
/// Creates a promise that resolves when at least one promise provided 
/// resolves. Arguments are passed as a parameter array
/// </summary>
/// <param name="promises" type="Object" paramArray="true">
/// A parameter array of objects to wait for
/// </param>
/// <returns type="Promise">
/// A Promise whose result is the result of the first promise to resolve or 
/// fail.
/// </returns>

var promise = Promise.all(...promises)
/// <summary>
/// Creates a promise that resolves when all of the provided promises resolve 
/// or fail. Arguments are passed as a parameter array
/// </summary>
/// <param name="promises" type="Object" paramArray="true">
/// A parameter array of objects to wait for
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an in-order array of the results of the 
/// promises provided
/// </returns>

var promise = Promise.work(url, message)
/// <summary>
/// Creates a Promise for a result posted by a Web Worker thread
/// </summary>
/// <param name="url" type="String">
/// The url to the script for the web worker
/// </param>
/// <param name="message" type="Object" optional="true" mayBeNull="true">
/// An optional message to send to start the worker
/// </param>
/// <returns type="Promise">
/// A Promise for the first message posted by the Web Worker
/// </returns>
/// <remarks>
/// This is a simplified approach to Web Worker-based promises. The first 
/// message posted by the worker becomes the result and the worker is then 
/// immediately terminated.
/// </remarks>

var promise = Promise.work(options)
/// <summary>
/// Creates a Promise for a result posted by a Web Worker thread
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url" and "message"
/// </param>
/// <returns type="Promise">
/// A Promise for the first message posted by the Web Worker
/// </returns>
/// <remarks>
/// This is a simplified approach to Web Worker-based promises. The first 
/// message posted by the worker becomes the result and the worker is then 
/// immediately terminated
/// </remarks>

var promise = Promise.get(url, username, password, headers, query)
/// <summary>
/// Issues an HTTP GET request for the content at the provided url
/// </summary>
/// <param name="url" type="String">The url for the content</param>
/// <param name="username" type="String" optional="true" mayBeNull="true">
/// The username for the request.
/// </param>
/// <param name="password" type="String" optional="true" mayBeNull="true">
/// The password for the request.
/// </param>
/// <param name="headers" type="Object" optional="true" mayBeNull="true">
/// A JSON object containing headers for the request.
/// </param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the 
/// request.
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.get(options)
/// <summary>
/// Issues an HTTP GET request for the content at the provided url
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url", "username", "password", 
/// "headers", and "query".
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.put(url, username, password, headers, query, body)
/// <summary>
/// Issues an HTTP PUT request for the content at the provided url
/// </summary>
/// <param name="url" type="String">The url for the content</param>
/// <param name="username" type="String" optional="true" mayBeNull="true">
/// The username for the request.
/// </param>
/// <param name="password" type="String" optional="true" mayBeNull="true">
/// The password for the request.
/// </param>
/// <param name="headers" type="Object" optional="true" mayBeNull="true">
/// A JSON object containing headers for the request.
/// </param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the request.
/// </param>
/// <param name="body" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the body for the request.
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.put(options)
/// <summary>
/// Issues an HTTP PUT request for the content at the provided url
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url", "username", "password", 
/// "headers", "query", and "body"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.post(url, username, password, headers, query, body)
/// <summary>
/// Issues an HTTP POST request for the content at the provided url
/// </summary>
/// <param name="url" type="String">The url for the content</param>
/// <param name="username" type="String" optional="true" mayBeNull="true">
/// The username for the request.
/// </param>
/// <param name="password" type="String" optional="true" mayBeNull="true">
/// The password for the request.
/// </param>
/// <param name="headers" type="Object" optional="true" mayBeNull="true">
/// A JSON object containing headers for the request.
/// </param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the request.
/// </param>
/// <param name="body" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the body for the request.
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.post(options)
/// <summary>
/// Issues an HTTP POST request for the content at the provided url
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url", "username", "password", 
/// "headers", "query", and "body"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.del(url, username, password, headers, query)
/// <summary>
/// Issues an HTTP DELETE request for the content at the provided url
/// </summary>
/// <param name="url" type="String">The url for the content</param>
/// <param name="username" type="String" optional="true" mayBeNull="true">
/// The username for the request.
/// </param>
/// <param name="password" type="String" optional="true" mayBeNull="true">
/// The password for the request.
/// </param>
/// <param name="headers" type="Object" optional="true" mayBeNull="true">
/// A JSON object containing headers for the request.
/// </param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the request.
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.del(options)
/// <summary>
/// Issues an HTTP DELETE request for the content at the provided url
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url", "username", "password", 
/// "headers", and "query"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is an XMLHttpRequest object
/// </returns>

var promise = Promise.json(method, url, username, password, headers, query, 
                           body)
/// <summary>
/// Issues an HTTP request for the content at the provided url, returning it 
/// as a JSON object
/// </summary>
/// <param name="method" type="String">
/// The HTTP method to use for the request. May be GET, PUT, POST, or DELETE
/// </param>
/// <param name="url" type="String">The url for the content</param>
/// <param name="username" type="String" optional="true" mayBeNull="true">
/// The username for the request.
/// </param>
/// <param name="password" type="String" optional="true" mayBeNull="true">
/// The password for the request.
/// </param>
/// <param name="headers" type="Object" optional="true" mayBeNull="true">
/// A JSON object containing headers for the request.
/// </param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the request.
/// </param>
/// <param name="body" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the body for the request.
/// </param>
/// <returns type="Promise">
/// A Promise whose result is a JavaScript object
/// </returns>

var promise = Promise.json(options)
/// <summary>
/// Issues an HTTP request for the content at the provided url, returning it 
/// as a JSON object
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "method", "url", "username", 
/// "password", "headers", "query", and "body"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is a JavaScript object
/// </returns>

var promise = Promise.jsonp(url, query, callbackArg)
/// <summary>
/// Issues an HTTP request using JSONP for the content at the provided url
/// </summary>
/// <param name="url" type="String">The url for the content</param>
/// <param name="query" type="Object" optional="true" mayBeNull="true">
/// A JSON object or String containing the querystring for the request.
/// </param>
/// <param name="callbackArg" type="String" optional="true">
/// The name of the callback argument. The default is "callback"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is a JavaScript object
/// </returns>

var promise = Promise.jsonp(options)
/// <summary>
/// Issues an HTTP request using JSONP for the content at the provided url
/// </summary>
/// <param name="options" type="Object">
/// An object literal for the arguments "url", "query", and "callbackArg"
/// </param>
/// <returns type="Promise">
/// A Promise whose result is a JavaScript object
/// </returns>