Freigeben über


Asynchronous Programming in JavaScript with “Promises”

Asynchronous patterns are becoming more common and more important to moving web programming forward. They can be challenging to work with in JavaScript. To make asynchronous (or async) patterns easier, JavaScript libraries (like jQuery and Dojo) have added an abstraction called promises (or sometimes deferreds). With these libraries, developers can use promises in any browser with good ECMAScript 5 support. In this post, we’ll explore how to use promises in your web applications using XMLHttpRequest2 (XHR2) as a specific example.

Benefits and Challenges with Asynchronous Programming

As an example, consider a web page that starts an asynchronous operation like XMLHttpRequest2 (XHR2) or Web Workers. There’s a benefit as some work happens “in parallel.” There’s complexity for the developer to keep the page responsive to people and not block human interaction while coordinating what the web page is doing with the asynchronous work. There’s complexity because program execution no longer really follows a simple linear path.

When you make an asynchronous call, you need to handle both successful completion of the work as well as any potential errors that may arise during execution. Upon the successful completion of one asynchronous call, you may want to pass the result into make another Ajax request. This can introduce complexity through nested callbacks.

function searchTwitter(term, onload, onerror) {

var xhr, results, url;

 

url = 'https://search.twitter.com/search.json?rpp=100&q=' + term;

 

xhr = new XMLHttpRequest();

xhr.open('GET', url, true);

 

xhr.onload = function (e) {

if (this.status === 200) {

results = JSON.parse(this.responseText);

onload(results);

}

};

 

xhr.onerror = function (e) {

onerror(e);

};

 

xhr.send();

}

 

function handleError(error) {

/* handle the error */

}

 

function concatResults() {

/* order tweets by date */

}

 

function loadTweets() {

var container = document.getElementById('container');

 

searchTwitter('#IE10', function (data1) {

searchTwitter('#IE9', function (data2) {

/* Reshuffle due to date */

var totalResults = concatResults(data1.results, data2.results);

 

totalResults.forEach(function (tweet) {

var el = document.createElement('li');

el.innerText = tweet.text;

container.appendChild(el);

});

}, handleError);

}, handleError);

}

The nested callbacks make the code hard to understand – what code is business logic specific to the app and what is the boilerplate code required to deal with the asynchronous call? In addition, due to the nested callbacks, the error handling becomes fragmented. We must check several places to see if an error occurred.

To reduce the complexity of coordinating asynchronous behavior, developers have looked for a way to perform consistent, easy to understand error handling with an alternative to nested callbacks.

Promises

One pattern is a promise, which represents the result of a potentially long running and not necessarily complete operation. Instead of blocking and waiting for the long-running computation to complete, the pattern returns an object which represents the promised result.

An example of this might be making a request to a third-party system where network latency is uncertain. Instead of blocking the entire application while waiting, the application is free to do other things until the value is needed. A promise implements a method for registering callbacks for state change notifications, commonly named the then method:

var results = searchTwitter(term).then(filterResults);

displayResults(results);

At any moment in time, promises can be in one of three states: unfulfilled, resolved or rejected.

To give an idea how the concept works, let’s start out with the CommonJS Promise/A proposal which has several derivatives in popular libraries. The then method on the promise object adds handlers for the resolved and rejected states. This function returns another promise object to allow for promise-pipelining, enabling the developer to chain together async operations where the result of the first operation will get passed to the second.

then(resolvedHandler, rejectedHandler);

The resolvedHandler callback function is invoked when the promise enters the fulfilled state, passing in the result from the computation. The rejectedHandler is invoked when the promise goes into the failed state.

We’ll revisit the example above using a pseudo code example of a promise to make the Ajax request to search Twitter, populate the screen with data, and handle errors. Let’s start with an example of what a promise library might look like if we were designing one from scratch with just the very basics. First we’ll need some form of object to keep the promise.

var Promise = function () {

/* initialize promise */

};

Next, we’ll need to implement the then method which allows us to chain together operations based upon state change of our promise. This method takes two functions for handling when the promise is resolved and for when the promise is rejected.

Promise.prototype.then = function (onResolved, onRejected) {

/* invoke handlers based upon state transition */

};

We’ll also need a couple of methods to perform a state transition between unfulfilled and resolved or rejected states.

Promise.prototype.resolve = function (value) {

/* move from unfulfilled to resolved */

};

 

Promise.prototype.reject = function (error) {

/* move from unfulfilled to rejected */

};

Now that we have some boilerplate for what a Promise object could be, let’s walk through the example from above of querying Twitter for #IE10 tagged tweets. First, we’ll create a method for making an Ajax GET request using the XMLHttpRequest2 to a given URL and wrap it in a promise. Next, we’ll create a method specifically for Twitter calling our Ajax wrapper method with a given search term. Finally, we’ll invoke our search function and display the results in an unordered list.

function searchTwitter(term) {

 

var url, xhr, results, promise;

url = 'https://search.twitter.com/search.json?rpp=100&q=' + term;

promise = new Promise();

xhr = new XMLHttpRequest();

xhr.open('GET', url, true);

 

xhr.onload = function (e) {

if (this.status === 200) {

results = JSON.parse(this.responseText);

promise.resolve(results);

}

};

 

xhr.onerror = function (e) {

promise.reject(e);

};

 

xhr.send();

 

return promise;

 

}

 

function loadTweets() {

var container = document.getElementById('container');

 

searchTwitter('#IE10').then(function (data) {

data.results.forEach(function (tweet) {

var el = document.createElement('li');

el.innerText = tweet.text;

container.appendChild(el);

});

}, handleError);

}

Now that we’re able to make a single Ajax request as a Promise, let’s discuss a scenario when we want to make more than one Ajax request and coordinate the results. To handle this scenario, we’ll create a when method on our Promise object to queue the promises for to be invoked. Once the promises move from unfulfilled to either resolved or rejected, the appropriate handler is called in the then method. The when method is essentially a fork-join operation that awaits completion of all of the operations before continuing.

Promise.when = function () {

/* handle promises arguments and queue each */

};

Now we can queue up multiple promises simultaneously, for example by searching for both #IE10 and #IE9 on Twitter.

var container, promise1, promise2;

 

container = document.getElementById('container');

 

promise1 = searchTwitter('#IE10');

promise2 = searchTwitter('#IE9');

 

Promise.when(promise1, promise2).then(function (data1, data2) {

/* Reshuffle due to date */

var totalResults = concatResults(data1.results, data2.results);

 

totalResults.forEach(function (tweet) {

var el = document.createElement('li');

el.innerText = tweet.text;

container.appendChild(el);

});

}, handleError);

The important thing to remember is that the code in these samples is nothing but normal JavaScript. Web developers certainly create their own Promise-like libraries; but for convenience and consistency you can leverage the promises patterns exposed in common JavaScript libraries.

Exploring Promises in jQuery and the Dojo Toolkit

There are many JavaScript libraries that are available to the developer which implement some form of a Promise. Let’s now explore a few libraries which expose promises or similar concepts.

The Dojo Toolkit

The first widespread use of this pattern was with the dojo toolkit deferred object in version 0.9. Just like the CommonJS Promises/A proposal above, this object exposes a then method which allows the developer to handle both the fulfillment and error states and chain the promises together. The dojo.Deferred object exposes two additional methods; resolve which fulfills the promise, and reject, which sends the promise into the rejected state. Below is an example of using the dojo.Deferred object to make an Ajax request to an URL and parse the results.

function searchTwitter(term) {

var url, xhr, results, def;

url = 'https://search.twitter.com/search.json?rpp=100&q=' + term;

def = new dojo.Deferred();

xhr = new XMLHttpRequest();

xhr.open('GET', url, true);

 

xhr.onload = function (e) {

if (this.status === 200) {

results = JSON.parse(this.responseText);

def.resolve(results);

}

};

 

xhr.onerror = function (e) {

def.reject(e);

};

 

xhr.send();

 

return def;

}

 

dojo.ready(function () {

var container = dojo.byId('container');

 

searchTwitter('#IE10').then(function (data) {

data.results.forEach(function (tweet) {

dojo.create('li', {

innerHTML: tweet.text

}, container);

});

});

});

Fortunately, some of the Ajax methods such as dojo.xhrGet already return a dojo.Deferred object, so you don’t need to worry about wrapping those methods yourself.

var deferred = dojo.xhrGet({

url: "search.json",

handleAs: "json"

});

 

deferred.then(function (data) {

/* handle results */

}, function (error) {

/* handle error */

});

The next concept that Dojo introduces is the dojo.DeferredList, which allows the developer to handle multiple dojo.Deferred objects at once and then return the results to the callback handler passed to the then function. This mirrors the when method we had on our own version of the promise object.

dojo.require("dojo.DeferredList");

 

dojo.ready(function () {

var container, def1, def2, defs;

container = dojo.byId('container');

 

def1 = searchTwitter('#IE10');

def2 = searchTwitter('#IE9');

defs = new dojo.DeferredList([def1, def2]);

 

defs.then(function (data) {

// Handle exceptions

if (!results[0][0] || !results[1][0]) {

dojo.create("li", {

innerHTML: 'an error occurred'

}, container);

return;

}

 

var totalResults = concatResults(data[0][1].results, data[1][1].results);

 

totalResults.forEach(function (tweet) {

dojo.create("li", {

innerHTML: tweet.text

}, container);

});

});

});

The Dojo Toolkit may have been the first one to implement this pattern, but there are other major libraries such as jQuery which have also exposed a similar pattern.

jQuery

jQuery introduced a new concept in version 1.5 called Deferred which is also a derivative implementation of the CommonJS Promises/A proposal. The Deferred object exposes a then method which allows the developer to handle both the fulfillment and error states. Like dojo, this object also exposes resolve and reject. The developer can create a Deferred object in jQuery by calling the $.Deferred function.

function xhrGet(url) {

var xhr, results, def;

def = $.Deferred();

xhr = new XMLHttpRequest();

xhr.open('GET', url, true);

 

xhr.onload = function (e) {

if (this.status === 200) {

results = JSON.parse(this.responseText);

def.resolve(results);

}

};

 

xhr.onerror = function (e) {

def.reject(e);

};

 

xhr.send();

 

return def;

}

 

function searchTwitter(term) {

var url, xhr, results, def;

url = 'https://search.twitter.com/search.json?rpp=100&q=' + term;

def = $.Deferred();

xhr = new XMLHttpRequest();

xhr.open('GET', url, true);

 

xhr.onload = function (e) {

if (this.status === 200) {

results = JSON.parse(this.responseText);

def.resolve(results);

}

};

 

xhr.onerror = function (e) {

def.reject(e);

};

 

xhr.send();

 

return def;

}

 

$(document).ready(function () {

var container = $('#container');

 

searchTwitter('#IE10').then(function (data) {

data.results.forEach(function (tweet) {

container.append('<li>' + tweet.text + '</li>');

});

});

});

Different from dojo, jQuery does not return another promise from the then method. Instead, jQuery provides the pipe method to chain operations together. Additionally, jQuery offers other utility methods which allow for richer composition including filtering through the pipe method as well as the jQuery style $ syntax.

The jQuery 1.5 release alters the Ajax methods to now return the jqXHR object which directly implements the promise interface.

$.ajax({

url: 'https://search.twitter.com/search.json',

dataType: 'jsonp',

data: { q: '#IE10', rpp: 100 }

}).then(function (data) {

/* handle data */

}, function (error) {

/* handle error */

});

For consistency, the jQuery.ajax method also provides the success, error, and complete methods.

$.ajax({

url: 'https://search.twitter.com/search.json',

dataType: 'jsonp',

data: { q: '#IE10', rpp: 100 }

}).success(function (data) {

/* handle data */

}).error(function (error) {

/* handle error */

});

Conclusion

There are many options available to the developer on how to deal with the complexities of asynchronous programming. With well-known patterns such as promises and deferred objects, and libraries that expose them, the developer is able to create rich interactions which seamlessly bridge asynchronous requests. In this example, we discussed leveraging promises and deferred objects atop XMLHttpRequests but the patterns could easily be layered on top of Web Workers, the setImmediate API, the FileAPI, or any other asynchronous API. You can use common JavaScript libraries so you don’t have to write boilerplate code.

The promises pattern is a good start, but it’s not the end of the solution. In fact, many patterns are emerging to address asynchronous programming. We think this is an interesting development which makes our lives easier as web developers and think it can help you write your web applications as well. Happy coding!

—Matt Podwysocki, JavaScript geek and consultant
—Amanda Silver, Program Manager for JavaScript

Comments

  • Anonymous
    September 11, 2011
    jQuery also has promises - instances of the Deferred have a .promise() method, which is what really should be returned from functions that are meant to be observable.  By returning the deferred, the consumer of the deferred can resolve or reject it, which they simply shouldn't be able to do.  By returning "def.promise()", the consumer can subscribe to the resolution/rejection of the deferred by using .then, .fail, .done, etc, without being able to manipulate it from outside of the function that created it.

  • Anonymous
    September 11, 2011
    Does this sound like IE10 will implement XMLHttpRequest2 (i.e. including the upload capabilities)? This would be nice :-)

  • Anonymous
    September 11, 2011
    No, this sounds like IE10 will still block the GUI when doing synchronous requests using XHR...

  • Anonymous
    September 11, 2011
    Why is this published here? Shouldn't this be on a more advanced blog like Mozilla or Chrome or Opera? Or just about anywhere else?

  • Anonymous
    September 11, 2011
    What gives?! Why are comment on the IE blog not working? I thought this was being fixed?

  • Anonymous
    September 11, 2011
    The comment has been removed

  • Anonymous
    September 11, 2011
    Microsoft, Please make it so that Internet Explorer 10 (IE10) supports text drop shadows.. like what is on twitter.com/newtwitter its the text new.. it has like a glow to the text

  • Anonymous
    September 12, 2011
    Doesn't the jQuery.when() simplify this significantly? api.jquery.com/jQuery.when

  • Anonymous
    September 12, 2011
    The comment has been removed

  • Anonymous
    September 12, 2011
    There's nothing here that a complete rewrite couldn't fix. Oh no, perhaps that's why this is taking so long? !!! MAKE THIS BLOG WORK !!!

  • Anonymous
    September 12, 2011
    @ieblog or @Ted Johnson (the guy who reply to the ieblog's emails), can you forward our complaints, about this blogging system, to the concerned people? It is understandable that it’s on least priority in your time table. Not to mention, its being years now people are complaining about this issue. IMO, One way to handle it quickly is by deploying the blogging suite developed by "telligent" (the same one deployed for windowsteamblog.com).

  • Anonymous
    September 12, 2011
    @DanglingPointer This blog is already running telligent, it appears to be a slightly newer version than windowsteamblog.com

  • Anonymous
    September 13, 2011
    Aaargh, the new IE10 version has already been shown on BUILD in Windows 8 so where are the posts on the blog ....

  • Anonymous
    September 13, 2011
    The comment has been removed

  • Anonymous
    September 13, 2011
    @fr, is there any reference for that? i mean on windowsteamblog.com, its mentioned "Powered By telligent". But here it’s none! Also, the comments on windowsteamblog.com never get missed from phone or from the computer.

  • Anonymous
    September 13, 2011
    I'd like to extend an olive branch in good faith to Microsoft regarding fixing this blog once and for all. Is there someone at Microsoft I can talk to about fixing the comment system on here so that posts are not constantly lost. It is currently not presenting Microsoft in a positive light when their flagship developer blog doesn't even work! We realize that the majority of blame points to 3rd party blog software but there are many simple solutions to fix it. If you plan to fix it on your own that's fine, please indicate you plan to do so and provide a timeline for the fix. If instead you would rather have some assistance please specify an email address where we can contact you to discuss (free as in beer help). Thanks steve_web PS sorry for cross posting on multiple threads but this issue needs serious attention ASAP.

  • Anonymous
    September 13, 2011
    @Danglingpointer If you right click a page here and view source you will see near the top a meta tag with name="GENERATOR" content="Telligent Community 5.6.583.17018 (Build: 5.6.583.17018)", on windowsteamblog.com it shows "Telligent Community 1.5.134.12674 (Build: 5.5.134.12674)".  Also it seems like windowsteamblog.com requires you to be signed in to comment, don't know if commented when signed in here is any more reliable?

  • Anonymous
    September 13, 2011
    @fr, that’s correct. I guess few people were referring to this as an issue for only guest users. That makes sense. Seems like some problem with the timeout of postback. They need to review and rehash the code. I guess the message is not conveyed to the concerned people. Otherwise it’s not a big deal to troubleshoot it for the guys who developed it. Anyways, thanks for the reply! :)