A look at promises in JavaScript

10 min. read

Caution! This article is 7 years old. It may be obsolete or show old techniques. It may also still be relevant, and you may find it useful! So it has been marked as deprecated, just in case.

Everybody is talking about promises in JavaScript as a replacement for callback hell. So I decided to take a look.

What are promises?

Promises are a software abstraction that makes working with asynchronous operations much more pleasant. Sync code is blocking, you have to wait until it finishes execution to continue with the next statement, while async code is non-blocking. When using callbacks we are mostly writing async code.

However, in async mode, the extra callback parameter confuses our idea of what is input and what is the return value. If an error is thrown, nobody is there to catch it (unless you write a try/catch statement for that).

A promise can be in three states:

  • pending - The initial state of a promise.
  • fulfilled - The state of a promise representing a successful operation.
  • rejected - The state of a promise representing a failed operation.

Once a promise is fulfilled or rejected, it is immutable (i.e. it can never change again).

With promises we go from code like this:


getTweetsFor('octopusinvitro', function (error, results) {
  // the rest of your code goes here.
});

to one where your functions return a value, called a promise, which represents the eventual results of that operation:


var promiseForTweets = getTweetsFor('octopusinvitro');

With promises, when you call a function which performs some long-running operation, instead of that function blocking and then eventually returning with the result of the operation, it will return immediately. But rather than passing back the result of the operation (which isn’t available yet) it passes back a promise. This promise is a sort of proxy, representing the future result of the operation. You would then register a callback on the promise, which will be executed by the promise once the operation does complete and the result is available.

Using .then()

To avoid callback hell, you use .then() to attach callbacks to a promise, whether for success or for errors (or even progress), as shown in the commonjs wiki:


.then(fulfilledHandler, errorHandler, progressHandler)

This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state. .then() causes errors to bubble up the stack by default.

Put simply, .then() is to .done() as .map() is to .forEach(). To put that another way, use .then() whenever you're going to do something with the result (even if that's just waiting for it to finish) and use .done() whenever you aren't planning on doing anything with the result. Another way to see it is state-machine vs. chained if-elses

But what are promises really?

At the end of the day, promises are about something much deeper, namely providing a direct correspondence between synchronous functions and asynchronous functions:

  • They return values
  • They throw exceptions

In an asynchronous world, you can no longer return values: they simply aren’t ready in time. Similarly, you can’t throw exceptions, because nobody’s there to catch them. With promises:

  • If either handler returns a value, the new promise is fulfilled with that value.
  • If either handler throws an exception, the new promise is rejected with that exception.

As mentioned at the beginning, promises can be in process, fulfilled or rejected:

  1. Fulfilled, fulfillment handler returns a value: simple functional transformation
  2. Fulfilled, fulfillment handler throws an exception: getting data, and throwing an exception in response to it
  3. Rejected, rejection handler returns a value: a catch clause got the error and handled it
  4. Rejected, rejection handler throws an exception: a catch clause got the error and re-threw it (or a new one)

Without these transformations being applied, you lose all the power of the synchronous/asynchronous parallel, and your so-called "promises" become simple callback aggregators. This is the problem with jQuery's current "promises": they only support scenario 1 above, entirely omitting support for scenarios 2–4.

See this beautiful kame-hame-ha:


step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

Instead of doing this, your functions should return a promise, which can do one of two things:

  • Become fulfilled by a value
  • Become rejected with an exception

Promises in jQuery

This is how you would do it in jQuery, until native promises are supported in all browsers:


function getData(url) {
  return $.get(url).promise();
}

function ifSuccess(userData, reposData) {
  user = userData;
  repos = reposData;
}

function ifFailure() {
  user = {};
  repos = {};
}

connection.get = function(userURL, reposURL) {
  $.when(
    getData(userURL),
    getData(reposURL)
  ).then(ifSuccess, ifFailure);

  return [user, repos];
};

If you have several promises, you can handle them in an array and wait until all promises are fullfiled:


var callback = function(results) {
  console.log(results);
}

var error = function() {
  console.log('There was an error');
}

$.when.all(requests).done(callback, error);

The beauty of a promise is that it is an encapsulated representation of an async operation in process which can be returned from functions, passed to functions, stored in a queue, etc. A promise separates the async code from the callback.

Promises in node

You can use promises today if you are using node with the promise library:


var Promise = require('promise');

The "promise" library also provides a couple of really useful extensions for interacting with node.js:


var readFile = Promise.denodeify(require('fs').readFile);
// now `readFile` will return a promise rather than expecting a callback

function readJSON(filename, callback){
  // If a callback is provided, call it with error as the first argument
  // and result as the second argument, then return `undefined`.
  // If no callback is provided, just return the promise.

  return readFile(filename, 'utf8').then(JSON.parse).nodeify(callback);
}

More patterns

You can find more promise patterns at the promisejs site.

Comments