OdeToCode IC Logo

Promises in ES2015 Part 1

Thursday, September 3, 2015

Asynchronous programming has been a hallmark of JavaScript programming since the beginning. Examples include waiting for button clicks, waiting for timers to expire, and waiting for network communications to complete. For most of JavaScript’s life, we’ve implemented these waiting activities using callback functions.

let calculate = function(callback) {

    setTimeout(() => {
        callback("This is the result"); 
    }, 0);

};

calculate(result => {
    expect(result).toBe("This is the result");
    done(); // done is required to complete 
            // this async test when using Jasmine
});

Over the years, as JavaScript applications grew in complexity, unofficial specifications started to emerge for a different approach to asynchronous programming using promises. A promise is an object that promises to deliver a result in the future. Promises are now an official part of the JavaScript language and offer advantages over the callback approach to asynchronous programming. Error handling is often easier using promises, and promises make it easier to compose multiple operations together. These advantages make code easier to read and write, as we’ll see in the upcoming posts.

Promise Objects

We need to look at promises from two different perspectives. The first perspective is the perspective of the code consuming a promise to wait for an asynchronous activity to complete. The second perspective is the perspective of the code responsible for producing a promise and managing an asynchronous activity behind the scenes.

A Promise Producer

In the previous example, the calculate function delivers a result asynchronously after a timer expires by invoking a callback function and passing along the result. When using promises, the calculate function could look like the following.

let calculate = function () {

    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(96);
        }, 0);
    });

};

Instead of taking a callback function as a parameter, the calculate method returns a new promise object. The Promise constructor takes a parameter known as the executor function. The previous code sample implements the executor function as an arrow function. The executor function itself takes two arguments. The first argument is the resolve function. Invoking the resolve function fulfills the promise and delivers a successful value to anyone who is waiting for a result from the promise. The second function is the reject function. Invoking the reject function rejects the promise, which signals an error. The above code always resolves the promise successfully and passes along a result of 96.

A Promise Consumer

Instead of passing a callback function to the calculate method, the consumer now invokes the calculate method and receives a promise object. A promise object provides an API that allows the consumer to execute code when the producer resolves or rejects the promise. The most important part of the promise API to a consumer is the then method. The then method allows the consumer to pass function arguments to execute when the promise resolves or rejects. The consumer code for calculate can now look like the following.

let success = function(result) {
    expect(result).toBe(96);
    done();
};

let error = function(reason) {
    // ... error handling code for a rejected promise
};

let promise = calculate();
promise.then(success, error);

The first argument to the then method is the success handler, while the second argument is the error handler.

On the surface, using promises might seem more involved than using simple callback functions. However, promises really start to shine when composing operations, and when handling errors. We’ll look at these topics in the coming posts.