BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Promises: The New Async Standard in Browser JavaScript?

Promises: The New Async Standard in Browser JavaScript?

This item in japanese

Lire ce contenu en français

Everybody who did more than some basic work with JavaScript has come across asynchronous programming: rather than a function returning a result value immediately, you pass in a callback function that is called when the result becomes available at some later time. Opinions on how to build large applications using asynchronous APIs has been a long ongoing debate in the JavaScript world. However, recently native promises were added to EcmaScript 6 (currently already available in the latest versions of Chrome, Firefox and Opera), and future browser APIs are adopting them. Will this settle the debate? Now that promises have become a "native" part of browser JavaScript will they become the new standard for asynchronous application code as well?

The Problem

What's asynchronous programming? Let's say we want to fetch a file mydata.json from the server from a web page. If you wouldn't be familiar with browser web development, you may have expected some sort of API like this:

var result = http.get("/mydata.json");
console.log("Got result", result);

However, on the web this is either impossible or considered bad practice. The reason is that the browser offers a primarily single-threaded runtime environment: you only have a single thread available to both render your web page, handle events and execute logic. Therefore, if you do expensive or slow things on that thread, your browser freezes for the duration. That is, if fetching your JSON file takes 2 seconds, the browser won't be able to do anything else than wait for the HTTP call. That is not a very good experience. To avoid this problem, most expensive calls in JavaScript are exposed as asynchronous APIs.

In general, the idea is to not block the main thread to wait for a response, but rather pass in a function that will be called at some later time when the result has been fetched. Therefore, while the HTTP calls is still going on, the browser can continue doing other things. Let's look at the classic XmlHTTPRequest example:

var req = new XMLHttpRequest();
req.open("GET", "/mydata.json", true);
req.onload = function(e) {
    var result = req.responseText;
    console.log("Got result", result);
};
req.send();

The pattern here is to create a request object, attach an event listener to it listening on the load event. When it triggers, you continue on with your work.

This request-object-with-event-emitters pattern is common in browser APIs, but there are also other patterns used in browser APIs. For instance, let's consider the Geolocation API that can be used to retrieve the user's current location:

navigator.geolocation.getCurrentPosition(function(result) {
    console.log("Location", result);
}, function(err) {
    console.error("Got error", err);
});

Rather than returning a request object, the getCurrentPosition API takes multiple callback functions, where the first one is called on successfully fetching the user's location and the second if something goes wrong.

On the server-side node.js has the de-facto standard of passing in a single callback function taking two arguments (error and result) as a last argument to an asynchronous API. For instance, here's how to read a file using node.js:

var fs = require("fs");

fs.readFile("mydata.json", function(err, content) {
    if (err) {
        console.error("Got an error", err);
    } else {
        console.log("Got result", content);
    }
});

All these patterns have challenges. For instance:

  • Propagation of errors is manual. In a synchronous programming style you can use throw, try and catch to handle and propagate errors. These language-level mechanism don't work well with asynchronous flows. Worse yet, forgetting to handle errors correctly can easily result in errors either disappearing or crashing the process.
  • Callback functions are never called or called multiple times. If you write asynchronous functions it is quite common to accidentally forget to call the callback function, or to call it multiple times. Both will result in very difficult to debug problems.
  • Callback functions naturally result in deeply nested callbacks. This can be avoided, but it is a common problem in asynchronous code.

The Promise

Promises aim to simplify writing asynchronous code. APIs that use promises don't take callback arguments, but instead return a promise object. A promise object only has a few methods, most importantly the then method (which is why promises are sometimes referred to as "thenables"). The then method takes one or two arguments, the first is called when the promise is resolved (succeeds) and the second is called if the promise is rejected (results in an error). Either of these callback functions can do any of the following:

  • Return a new promise. In this case the result of the promise (resolving or rejecting) is delegated to this new returned promise. In effect this can be used to easily chain asynchronous call without requiring deep nesting.
  • Return a (non-promise) value. In this case the promise is resolved to this value.
  • Throw an error. If an error is thrown inside of the body of the function, this will result in rejecting the promise.

Let's look at an example. Let's say we want to retrieve the user's location and then perform an AJAX call to the server with that location. The following code performs this task using regular style asynchronous programming:

navigator.geolocation.getCurrentPosition(function(location) {
    var req = new XMLHttpRequest();
    req.open("PUT", "/location", true);
    req.onload = function() {
        console.log("Posted location!");
    };
    req.onerror = function(e) {
        console.error("Putting failed", e);
    };
    req.send(JSON.stringify(location.coords));
}, function(err) {
    console.error("Got error", err);
});

As you can see we have two error handlers here. Now let's look at a version using a hypothetical promise-based version of these APIs:

navigator.geolocation.getCurrentPosition().then(function(location) {
    var req = new XMLHttpRequest();
    req.open("PUT", "/location", true);
    return req.send(JSON.stringify(location.coords));
}).then(function() {
    console.log("Posted location!");
}).then(null, function(err) {
    console.error("Got error", err);
});

A few things to notice about the promise-based version:

  • Nesting is only one level deep.
  • Error handling is centralized in one place. If an error occurrs in the first call, it is propagated until a then call that handles it.

Promises have more advantages, see pointers to other material at the end of this article.

The Future

As of Chrome 32, Firefox 29 and Opera 19 Promises are now built into the browser using the Promise constructor. In addition, future browser APIs will use them. Examples include:

As promises become more prevalent in browser APIs, will more browser-based applications and libraries adopt them? jQuery already has promise-like support in terms of deferred. In addition, with EcmaScript 6's generators, promises get even more benefits, that is: the ability to write synchronous-looking code that is executed asynchronously.

To learn more about the new native Promises in JavaScript and their advantages, Jake Archibald's HTML5Rocks article is a good starting point. If you're targeting older browsers there's a small polyfill available. Domenic Denicola has given many great talks on promises and has been a advocate for promises for a long time. An API spec of promises can be found on the Promises/A+ proposal they are based on and Mozilla's Developer Network.

Rate this Article

Adoption
Style

BT