Promises For Asynchronous Code Execution
By Brian Mearns

Promises is a pattern for asynchronously executing code, and following up on the results of that execution. In this post, we’ll explore promises at an introductory level, based on the Promises/A+ specification, a widely adopted standard for promises.

A note on implementation: The promises pattern can be applied in a variety of different programming languages, but requires an implementation to support it. There’s nothing magical about the implementation, and promises libraries are available for a variety of languages. This post focuses more on the promises concept than on implementation, but you can learn more about implementation through open source projects such as those listed on the Promises/A+ website.

Overview

A promise is exactly what it sounds like: a promise to do something. You aren’t necessarily promising to do it right now; you’re just promising to do it at some point. In this way, it’s a lot like a future object in Java and other languages. It’s generally meant to encapsulate asynchronous work that needs to be done.

States, values, and reasons

promises-states

When a promise is first created, it’s in a pending state, meaning you haven’t yet done what you promised to do. After you do what you promised, the promise is said to be fulfilled. Sometimes the thing we’re promising to do will result in a value. For instance, if we promise to fetch something from a database, then fulfilling that promise might result in the data that was fetched. In this case, the promise is said to be fulfilled with the value.

Of course, we don’t always do what we promise to do; sometimes we break our promise. If we fail to do what we promised for any reason, then the promise is said to be rejected. We don’t always break our promises, but when we do we have our reasons. When a promise is rejected, it is rejected for a reason. That reason is typically an error object of some kind.

If a promise is in either the fulfilled or rejected states, it is said to be settled, which simply means we aren’t waiting on it anymore.

A promise only ever makes a single state transition in its lifetime, either from pending to fulfilled, or from pending to rejected. Once a promise is settled (fulfilled or rejected), it does not change states again.

Following up on promises

If you’ve worked with future objects in Java or other languages, you’re used to having accessor APIs to check the state and get the resulting value. A common pattern is to run a method that returns a future object, then do some other work while you wait for the future object to be resolved. Sooner or later, you end up in a blocking call or idle loop waiting for the future object to resolve with its value.

Promises takes a fundamentally different approach based entirely on callback chains. In fact, there’s no standard promises API for getting the state, fulfilled value, or reason for rejection. Instead, you can use handler functions passed as arguments to a promise’s then method. The first argument is called the onFulfill handler, and it will only be called if the promise is fulfilled, passing in the value with which it was fulfilled.

The second argument to then is optional, and is called the onReject handler. If given, the onReject handler will only be called if the promise is rejected, passing in the reason for which it was rejected.

var p = promiseToDoSomething();

var onFulfill = function(val) {
    log("Promise was fulfilled and produced the value: " + val); 
};
 
var onReject = function(reason) {
    log("Promise was rejected for reason: " + reason);
};
 
p.then(onFulfill, onReject);

Note: Unlike event handlers (which typically get called only if they are already registered when the event actually fires), the onFulfill and onReject handlers are called regardless of when the promise actually settles, even if the promise is already settled when the to then method is called.

Chaining promises

This is where the real power of promises comes in.

A promise’s then method itself returns a promise:  A promise to call one of the specified handler callbacks once the original promise is settled.

var p = promiseToDoSomething();
 
// q is a promise to "do something" and then to "do something else".
var q = p.then(function() {
    log('do something else');
});

// r is a promise to "do something", then "do something else",
// and finally to "do this last thing".
var r = q.then(function() {
    log('do this last thing');
});

The above code can be written more succinctly by chaining the calls to then, as follows:

promiseToDoSomething()
    .then(function() {
        log('do something else');
    })
    .then(function() {
        log('do this last thing');
    });

The sequence of promises created by such a chain of method calls is called a promise chain. We’ll look at promise chains in more detail later.

Promising to transform values

If it’s fulfilled, the promise returned by then will fulfill with the value returned by the handler you give it. In this way, chaining promises can be used to transform values.

The example below starts with a hypothetical function that returns a promise to fulfill with the current price in dollars of a specific stock, and uses promise chaining to transform this value into euros:

promiseToGetCurrentStockPrice(stockSymbol)
    .then(function(priceInDollars) {
        return dollarsToEuros(priceInDollars);
    })
    .then(function(priceInEuros) {
        log('The current stock price in euros: ' + priceInEuros);
    });

Asynchronous transformations

Let’s say the handler you want to pass to then also has to do something asynchronous. You probably want to use a promise for this, as well.

The then method makes this easy. Simply return the new promise, and it will integrate seamlessly into the chain.

Here’s an example, where we want to get the price of a particular stock on the day a user was born. First, we need to make an asynchronous call to our back end to get the user’s birthday. Then we need to make another asynchronous call to an API that returns the stock price for a given date:

promiseToGetUserBirthday(userId)
    .then(function(birthdate) {
        return promiseToGetStockPrice(stockSymbol, birthdate);
    })
    .then(function(stockPrice) {
        log('Stock price on the day you were born: ' + stockPrice);
    });

Notice that the promise returned by the first then does not get fulfilled with a promise as the value, even though the handler returned a promise. This is evidenced by the fact that the second handler expects to get a stock price as the fulfilled value. The promises library detects this special case return value from the handler and intercedes to “unwrap” the promise before it gets to the next handler.

It’s kind of like the next then is actually getting invoked on the promise that the first then returned – but not quite. Let’s take a deeper dive into what’s really happening.

A deeper dive

Remember, functions passed to then are handlers: instead of being called from inside the then method, they’re simply registered to be called later. The next then in the chain can’t really be invoked on the promise returned from a previous handler, because that handler hasn’t been invoked yet, and therefore the promise the handler is going to return doesn’t exist yet.

What’s actually happening is that when a handler returns a promise, the promise chain adopts the state of the returned promise, before continuing on. So, if the returned promise is still pending, the promise chain will be in the pending state and will not invoke the next handler until it settles. Once the promise returned by the handler settles, the chain will either fulfill with the same value, or reject for the same reason, and then continue on with the rest of the chain.

Let’s break up the first part of our previous example chain more explicitly to examine this behavior in detail.

var p = promiseToGetUserBirthday(userId);

function userBirthdayFulfilledHandler(birthdate) {
    var q = promiseToGetStockPrice(stockSymbol, birthdate);
    return q;
}

var r = p.then(userBirthdayFulfilledHandler);

Variable p holds a promise to get the user’s birthday. We invoke the then method on this promise to register our function userBirthdayFulfilledHandler as an onFulfill handler for promise p. The result of this method call is a new promise, r.

Upon creation, r starts in the pending state. It needs to wait until our handler is called before it can settle, because how it settles will depend on what the handler returns. If the handler returned a direct value instead of a promise, then r would immediately fulfill with that value. If the handler threw an error, then r would immediately reject for that reason.

In the case above, the handler is returning a promise, which we’ve labeled q. Because the handler returns a promise, r is going adopt the state of the returned promise. In other words, r not only has to wait for the handler to return, but also has to wait for the returned promise q to settle. Until then, r remains pending.

Once the returned promise q does settle, r will settle in the same way. If q is fulfilled, then r will fulfill with the same value. If q is rejected, then r will reject for the same reason.

Learning to cope with rejection

There’s a misconception that once a promise chain has a rejection in it, it stays rejected, but that’s not necessarily the case: you can use the onReject handler to return the chain to a fulfilled state.

You can think of the onReject handler as something like a catch block; it’s meant for handling problems. It is assumed that the onReject handler is taking care of the problem, so once it returns, the promise chain goes back to being fulfilled (unless the handler returned a promise, in which case it adopts the state of the returned promise).

The sample below shows an onReject handler being used to handle a rejection, causing the chain to return to a fulfilled state:

promiseToTryToDoSomething()
    .then(
        // onFulfill handler
        function(value) {
            log('The first promise fulfilled with value ' + value);
            return 'foo';
        },
        
        // onReject handler
        function(reason) {
            log('The first promise rejected because ' + reason);
            return 'bar';
        }
    )
    .then(function(value) {
        log('The second promise in the chain fulfilled!');
        log('Either with value "foo" or value "bar".');
        log(value); //either 'foo' or 'bar'
    });

If you actually want to cause the next promise in the chain to be rejected, then you can throw an error from either handler. This will be caught when the handler is executed, and used as the reason for rejection.

Alternatively, either one of your handlers could return a promise. If that promise ends up being rejected, then the chain will adopt that same state.

Making promises

The constructor for promises is not standardized in the A+ spec, but a common pattern among implementations is to pass a function that performs the work you’re promising to do (which we’ll call the work function).

The promise implementation will trigger your work function to be invoked with two arguments: fulfill and reject. These arguments are themselves functions, which will settle the state of the promise.

The work function you provide to the constructor should do its work, then call the passed-in fulfill function to indicate that the promise was successfully fulfilled, optionally providing the value with which it should be fulfilled. Here’s an example.

new Promise(function(fulfill, reject) {
    // Do the work you promised to do.

    // Then indicate that you fulfilled your promise with 'some value'.
    fulfill('some value');  
});

If a problem prevents you from keeping your promise, then your work function should call the provided reject function, passing in the reason for which it was rejected (typically an error object of some kind):

new Promise(function(fulfill, reject) {
    // Try to do the work you promised to do.

    // Oops, something went wrong, so you can't keep your promise:
    reject(new Error('reasons'));
});

Alternatively, if an error gets thrown inside your work function, the promise implementation will typically catch it and cause the promise to be rejected, using the caught error object as the reason:

new Promise(function(fulfill, reject) {
    // Try to do the work you promised to do.

    // Oops, something went wrong, so you can't keep your promise:
    throw new Error('reason for rejection');
});

Common API extensions

According to the spec, a promise is only required to provide the then method, but implementations often provide additional methods. Here are a few of those methods.

catch(onReject)

The catch method takes only an onReject handler as its argument. Calling p.catch(onReject) is the same as calling p.then(null, onReject).

done(onFulfill, onReject)

The done method is like then, except it doesn’t return a promise. It takes an onFulfill handler and an optional onReject handler, and calls the appropriate handler when the promise settles.

finally(onSettled)

The finally method enables you to use the same handler regardless of whether the promise is fulfilled or rejected. In other words, it uses a single handler function as both the onFulfill and onReject handler, so calling p.finally(handler) is the same as calling p.then(handler, handler).

Promise.all(listOfPromises)

The Promise.all method takes a list of promises, and returns a promise to wait for all of them to fulfill. If any promise rejects, the returned promise rejects with that reason. Meanwhile, if all the promises fulfill, then the promise returned by all will fulfill with a corresponding list of the fulfilled values.

Promise.race(listOfPromises)

The Promise.race method takes a list of promises, and returns a promise which adopts the state of whichever settles first. If the first settling promise is fulfilled, then the returned promise will be fulfilled with the same value. If the first settling promise is rejected, then the returned promise will be rejected for the same reason.

Note that all of the passed promises are still “executed,” so any side effects they may have still occur, but not necessarily before the returned promise settles.

Promise.resolve(fulfilledValue)

The Promise.resolve method is a factory method to create a promise that always fulfills with the given value.

Calling Promise.resolve(value) is the same as:

new Promise(function(fulfill, reject) {
    fulfill(value);
});

Alternatively, if value is itself a promise, then the returned promise will adopt its state, as when a handler passed to the then method returns a promise.

Promise.reject(reason)

The Promise.reject method is a factory method to create a promise that always rejects for the given reason.

Calling Promise.reject(reason) is the same as:

new Promise(function(fulfill, reject) {
    reject(reason);
});

References

Categories Software EngineeringTags , ,

Leave a Reply

Your email address will not be published. Required fields are marked *