Introducing Promises - JavaScript with Promises (2015)

JavaScript with Promises (2015)

Chapter 2. Introducing Promises

The biggest challenge with nontrivial amounts of async JavaScript is managing execution order through a series of steps and handling any errors that arise. Promises address this problem by giving you a way to organize callbacks into discrete steps that are easier to read and maintain. And when errors occur they can be handled outside the primary application logic without the need for boilerplate checks in each step.

A promise is an object that serves as a placeholder for a value. That value is usually the result of an async operation such as an HTTP request or reading a file from disk. When an async function is called it can immediately return a promise object. Using that object, you can register callbacks that will run when the operation succeeds or an error occurs.

This chapter covers the basic ways to use promises. By the end of the chapter you should be comfortable working with functions that return promises and using promises to manage a sequence of asynchronous steps.

This book uses the Promise API for the version of JavaScript known as ECMAScript 6 (ES6.) However, there were a number of popular JavaScript Promise libraries that the development community created before ES6 that may not match the spec. These differences are mostly trivial so it is generally easy to work with different implementations once you are comfortable using standard promises. We discuss API variations and compatibility issues with other libraries in Chapter 4.

Basic Usage

Let’s walk through the basics of Promises using a series of examples beginning with a traditional callback approach and moving to an implementation using promises. Example 2-1 loads an image in a web browser and invokes a success or error callback based on the outcome.

Example 2-1. Using callbacks

loadImage('shadowfacts.png',

function onsuccess(img) {

// Add the image to the current web page

document.body.appendChild(img);

},

function onerror(e) {

console.log('Error occurred while loading image');

console.log(e);

}

);

function loadImage(url, success, error) {

var img = new Image();

img.src = url;

img.onload = function () {

success(img);

};

img.onerror = function (e) {

error(e);

};

}

The loadImage function uses an HTML Image object to load an image by setting the src property. The browser asynchronously loads the image based on the src and queues the onload or onerror callback after it’s done.

Since loadImage is asynchronous, it accepts callbacks instead of immediately returning the image from the function. However, if loadImage was changed to return a promise you would attach the callbacks to the promise instead of passing them as arguments to the function. Example 2-2shows how loadImage is used when it returns a promise.

Example 2-2. Promise then and catch

// Assume loadImage returns a promise

var promise = loadImage('the_general_problem.png');

promise.then(function (img) {

document.body.appendChild(img);

});

promise.catch(function (e) {

console.log('Error occurred while loading image');

console.log(e);

});

The code indicates the following: “Load an image, then add it to the document or show an error if it can’t be loaded.” The promise that loadImage returns has a then method for registering a callback to use when the operation succeeds and a catch method for handling errors. However, both then and catch return promise objects so callback registration is usually done by chaining these method calls together, as shown in Example 2-3.1

Example 2-3. Chaining calls using then and catch

loadImage('security_holes.png').then(function (img) {

document.body.appendChild(img);

}).catch(function (e) {

console.log('Error occurred while loading image');

console.log(e);

});

And Example 2-4 is an implementation for loadImage that returns a promise.

Example 2-4. Creating and resolving a promise

function loadImage(url) {

var promise = new Promise(

function resolver(resolve, reject) {

var img = new Image();

img.src = url;

img.onload = function () {

resolve(img);

};

img.onerror = function (e) {

reject(e);

};

}

);

return promise;

}

A global constructor function called Promise exposes all the functionality for promises. In this example, loadImage creates a new promise and returns it. When Promise is used as a constructor it requires a callback known as a resolver function. The resolver serves two purposes: it receives the resolve and reject arguments, which are functions used to update the promise once the outcome is known, and any error thrown from the resolver is implicitly used to reject the promise. All the logic that was originally done in loadImage is now done inside the resolver. The resolve function is called when the image loads and reject is called if the image cannot be loaded.

When an operation represented by a promise completes, the result is stored and provided to any callbacks the promise invokes. The result is passed to the promise as a parameter of the resolve or reject functions. In the case of loadImage, the image is passed to resolve, so any callbacks registered with promise.then() will receive the image.

Multiple Consumers

When multiple pieces of code are interested in the outcome of the same async operation, they can use the same promise. For example, you can retrieve a user’s profile from the server and use it to display her name in a navigation bar. That data can also be used on an account page that displays her full profile. The code in Example 2-5 demonstrates this by using a promise to track whether a user’s profile has been received. Two independent functions use the same promise to display data once it is available.

Example 2-5. One promise with multiple consumers

var user = {

profilePromise: null,

getProfile: function () {

if (!this.profilePromise) {

// Assume ajax() returns a promise that is eventually

// fulfilled with {name: 'Samantha', subscribedToSpam: true}

this.profilePromise = ajax(/*someurl*/);

}

return this.profilePromise;

}

};

var navbar = {

show: function (user) {

user.getProfile().then(function (profile) {

console.log('*** Navbar ***');

console.log('Name: ' + profile.name);

});

}

};

var account = {

show: function (user) {

user.getProfile().then(function (profile) {

console.log('*** Account Info ***');

console.log('Name: ' + profile.name);

console.log('Send lots of email? ' + profile.subscribedToSpam);

});

}

};

navbar.show(user);

account.show(user);

// Console output:

// *** Navbar ***

// Name: Samantha

// *** Account Info ***

// Name: Samantha

// Send lots of email? true

Here a user object with a profilePromise property and a getProfile method is created. The getProfile method returns a promise that is resolved with an object containing the user profile information. Then the script passes the user to the navbar and account objects, which display information from the profile.

Remember that a promise serves as a placeholder for the result of an operation. In this case, the user.profilePromise is a placeholder used by the navbar.show() and account.show() functions. These functions can be safely called anytime before or after the profile data is available. The callbacks they use to print the data to the console will only be invoked once the profile is loaded. This removes the need for an if statement in either function to check whether the data is ready.

In addition to that simplification, using the promise placeholder has another benefit. It removes the need for signaling inside the getProfile function to display the username and profile once the data is ready. The promise implicitly provides that logic, happily decoupled from the details of how or when the data is displayed.

Promise States

The state of an operation represented by a promise is stored within the promise. At any given moment an operation has either not begun, is in progress, has run to completion, or has stopped and cannot be completed. These conditions are represented by three mutually exclusive states:

Pending

The operation has not begun or is in progress.

Fulfilled

The operation has completed.

Rejected

The operation could not be completed.

Figure 2-1 shows the relationship between the three states.

State diagram showing Pending, Fulfilled and Rejected.

Figure 2-1. Promise states

In the examples so far, we refer to the fulfilled and rejected states as success and error, respectively. There is a difference between these terms. An operation could complete with an error (although that may be bad form) and an operation may not complete because it was cancelled even though no error occurred. Hence, the terms fulfilled and rejected are better descriptions for these states than success and error.

When a promise is no longer pending it is said to be settled. This is a general term indicating the promise has reached its final state. Once a pending promise is settled the transition is permanent. Both the state and any value given as the result cannot be changed from that point on. This behavior is consistent with how operations work in real life. A completed operation cannot become incomplete and its result does not change. Of course a program may repeat the steps of an operation multiple times. For instance, a failed operation may be retried and multiple tries may return different values. In that case, a new promise represents each try, so a more descriptive way to think of a promise is a placeholder for the result of one attempt of an operation.

The code in Example 2-6 demonstrates how the state of a promise can only be changed once. The code calls resolve and reject in the same promise constructor. The call to resolve changes the state of the promise from pending to fulfilled. Any further calls to resolve or reject are ignored because the promise is already fulfilled.

Example 2-6. The state of a promise never changes after it is fulfilled or rejected

var promise = new Promise(function (resolve, reject) {

resolve(Math.PI);

reject(0); // Does nothing

resolve(Math.sqrt(-1)); // Does nothing

});

promise.then(function (number) {

console.log('The number is ' + number);

});

// Console output:

// The number is 3.141592653589793

Running the code in this example demonstrates that the calls to reject(0) and resolve(Math.sqrt(-1)) have no effect because the promise has already been fulfilled with a value for Pi.

The immutability of a settled promise makes code easier to reason about. Allowing the state or value to change after a promise is fulfilled or rejected would introduce race conditions. Fortunately, the state transition rules for promises prevent that problem.

Since the reject function transitions a promise to the rejected state, why does the resolve function transition a promise to a state called fulfilled instead of resolved? Resolving a promise is not the same as fulfilling it. When the argument passed to resolve is a value, the promise is immediately fulfilled. However, when another promise is passed to resolve, such as promise.resolve(otherPromise), the promises are bound together. If the promise passed to resolve is fulfilled, then both promises will be fulfilled. And if the promise passed to resolve is rejected, then both promises will be rejected. In short, the argument passed to resolve dictates the fate of the promise. Figure 2-2 shows this process.

When a promise is passsed to +resolve+ its outcome is propogated to the other promise.

Figure 2-2. Resolving or rejecting a promise

The resolve and reject functions can be called without an argument, in which case the fulfillment value or rejection reason will be the JavaScript type undefined.

The Promise API also provides two convenience methods (see Example 2-7) for creating a promise that is immediately resolved or rejected: Promise.resolve() and Promise.reject().

Example 2-7. Convenience functions for resolve and reject

// Equivalent ways to create a resolved promise

new Promise(function (resolve, reject) {

resolve('the long way')

});

Promise.resolve('the short way');

// Equivalent ways to create a rejected promise

new Promise(function (resolve, reject) {

reject('long rejection')

});

Promise.reject('short rejection');

These convenience functions are useful when you already have the item that should be used to resolve or reject the promise. Some of the code samples that follow use these functions instead of the traditional Promise constructor to concisely create a promise with the desired state.

Chaining Promises

We’ve seen how then and catch return promises for easy method chaining, however they do not return a reference to the same promise. Every time either of these methods is called a new promise is created and returned. Example 2-8 is an explicit example of then returning a new promise.

Example 2-8. Calls to then always return a new promise

var p1, p2;

p1 = Promise.resolve();

p2 = p1.then(function () {

// ...

});

console.log('p1 and p2 are different objects: ' + (p1 !== p2));

// Console output:

// p1 and p2 are different objects: true

Example 2-9 shows how new promises returned by then can be chained together to execute a sequence of steps.

Example 2-9. Using then to sequence multiple steps

step1().then(

function step2(resultFromStep1) {

// ...

}

).then(

function step3(resultFromStep2) {

// ...

}

).then(

function step4(resultFromStep3) {

// ...

}

);

Each call to then returns a new promise you can use to attach another callback. Whatever value is returned from that callback resolves the new promise. This pattern allows each step to send its return value to the next step. If a step returns a promise instead of a value, the following step receives whatever value is used to fulfill that promise. Example 2-10 shows all the ways to fulfill a promise created by then.

Example 2-10. Passing values in a sequence of steps

Promise.resolve('ta-da!').then(

function step2(result) {

console.log('Step 2 received ' + result);

return 'Greetings from step 2'; // Explicit return value

}

).then(

function step3(result) {

console.log('Step 3 received ' + result); // No explicit return value

}

).then(

function step4(result) {

console.log('Step 4 received ' + result);

return Promise.resolve('fulfilled value'); // Return a promise

}

).then(

function step5(result) {

console.log('Step 5 received ' + result);

}

);

// Console output:

// Step 2 received ta-da!

// Step 3 received Greetings from step 2

// Step 4 received undefined

// Step 5 received fulfilled value

An explicitly returned value resolves the promise that wraps step2. Since step3 does not explicitly return a value, undefined fulfills that promise. And the value from the promise explicitly returned in step4 fulfills the promise that wraps step4.

Callback Execution Order

Promises are primarily used to manage the order in which code is run relative to other tasks. The previous chapter demonstrated how problems occur when async callbacks are expected to run synchronously. You can avoid these problems by understanding which callbacks in the Promise API are synchronous and which are asynchronous. Fortunately there are only two cases. The resolver function passed to the Promise constructor executes synchronously. And all callbacks passed to then and catch are invoked asynchronously. Example 2-11 shows a Promise constructor and an onFulfilled callback with some logging statements to demonstrate the order. The numbered comments show the relative execution order.

Example 2-11. Execution order of callbacks used by promises

var promise = new Promise(function (resolve, reject) {

console.log('Inside the resolver function'); // 1

resolve();

});

promise.then(function () {

console.log('Inside the onFulfilled handler'); // 3

});

console.log('This is the last line of the script'); // 2

// Console output:

// Inside the resolver function

// This is the last line of the script

// Inside the onFulfilled handler

This example is similar to the synchronous and asynchronous callback code in the previous chapter. You can see that the resolver function passed to the Promise constructor executes immediately followed by the log statement at the end of the script. Then the event loop turns and the promise that is already resolved invokes the onFulfilled handler. Although the example code is trivial, understanding the execution order is a key part of using promises effectively. If you do not feel confident predicting the execution order of any of the examples so far, consider reviewing the material in Chapter 1 and this section.

Basic Error Propagation

Error propagation and handling is a significant aspect of working with promises. This section introduces the basic concepts while all of Chapter 5 is dedicated to this topic.

Rejections and errors propagate through promise chains. When one promise is rejected all subsequent promises in the chain are rejected in a domino effect until an onRejected handler is found. In practice, one catch function is used at the end of a chain (see Example 2-12) to handle all rejections. This approach treats the chain as a single unit that the fulfilled or rejected final promise represents.

Example 2-12. Using a rejection handler at the end of a chain

Promise.reject(Error('bad news')).then(

function step2() {

console.log('This is never run');

}

).then(

function step3() {

console.log('This is also never run');

}

).catch(

function (error) {

console.log('Something failed along the way. Inspect error for more info.');

console.log(error); // Error object with message: 'bad news'

}

);

// Console output:

// Something failed along the way. Inspect error for more info.

// [Error object] { message: 'bad news' ... }

This code begins a chain of promises by creating a rejected promise using Promise.reject(). Two more promises follow that are created by adding calls to then and finished with a call to catch to handle rejections.

Notice the code in step2 and step3 never runs. These functions are only called when the promise they are attached to is fulfilled. Since the promise at the top of the chain was rejected, all subsequent callbacks in the chain are ignored until the catch handler is reached.

Promises are also rejected when an error is thrown in a callback passed to then or in the resolver function passed to the Promise constructor. Example 2-13 is similar to the last, except throwing an error instead of using the Promise.reject() function now rejects the promise.

Example 2-13. Rejecting a promise by throwing an error in the constructor callback

rejectWith('bad news').then(

function step2() {

console.log('This is never run');

}

).catch(

function (error) {

console.log('Foiled again!');

console.log(error); // Error object with message: 'bad news'

}

);

function rejectWith(val) {

return new Promise(function (resolve, reject) {

throw Error(val);

resolve('Not used'); // This line is never run

});

}

// Console output:

// Foiled again!

// [Error object] { message: 'bad news' ... }

Both examples in this section provided a JavaScript Error object when rejecting the promise. Although any value, including undefined, can reject promises, we recommend using an error object. Creating an error can capture the call stack for troubleshooting and makes it easier to treat the argument the catch handler receives in a uniform way.

TIP

Using JavaScript Error objects to reject promises can capture the call stack for troubleshooting and makes it easier to treat the argument the catch handler receives in a uniform way.

The Promise API

The complete Promise API consists of a constructor and six functions, four of which have already been demonstrated. However, it’s worth describing each of them so you can see the API as a whole and be aware of optional arguments.

Promise

new Promise(function (resolve, reject) { … }) returns promise

The Promise global is a constructor function that the new keyword invokes.

The Promise global creates promise objects that have the two methods then and catch for registering callbacks that are invoked once the promise is fulfilled or rejected.

promise.then

promise.then([onFulfilled], [onRejected]) returns promise

The promise.then() method accepts an onFulfilled callback and an onRejected callback. People generally register onRejected callbacks using promise.catch() instead of passing a second argument to then (see the explanation provided in Chapter 5.) The function thenreturns a promise that is resolved by the return value of the onFulfilled or onRejected callback. Any error thrown inside the callback rejects the new promise with that error.

promise.catch

promise.catch(onRejected) returns promise

The promise.catch() method accepts an onRejected callback and returns a promise that the return value of the callback or any error thrown by the callback resolves or rejects, respectively. That means any rejection the callback given to catch handles is not propagated further unless you explicitly use throw inside the callback.

Promise.resolve

Promise.resolve([value|promise]) returns promise

The Promise.resolve() function is a convenience function for creating a promise that is already resolved with a given value. If you pass a promise as the argument to Promise.resolve(), the new promise is bound to the promise you provided and it will be fulfilled or rejected accordingly.

Promise.reject

Promise.reject([reason]) returns promise

The Promise.reject() function is a convenience function for creating a rejected promise with a given reason.

Promise.all

Promise.all(iterable) returns promise

The Promise.all() function maps a series of promises to their fulfillment values. It accepts an iterable object such as an Array, a Set, or a custom iterable. The function returns a new promise fulfilled by an array containing the values in the iterable. Corresponding fulfillment values in the resulting array replace any promises contained in the iterable. The new promise that the function returns is only fulfilled after all the promises in the iterable are fulfilled, or it is rejected as soon as any of the promises in the iterable are rejected. If the new promise is rejected it contains the rejection reason from the promise in the iterable that triggered the rejection. If you are working with a Promise implementation that does not understand ES6 iterables, it will likely expect standard arrays instead.

WHAT IS AN ITERABLE?

An iterable is an object that provides a series of values by implementing a predefined interface (also known as a protocol.) Iterables are specified in ES6 and explained in “Iterables and Iterators”.

Promise.race

Promise.race(iterable) returns promise

The Promise.race() function reduces a series of items to the first available value. It accepts an iterable and returns a new promise. The function examines each item in the iterable until it finds either an item that is not a promise or a promise that has been settled. The returned promise is then fulfilled or rejected based on that item. If the iterable only contains unsettled promises, the returned promise is settled once one of the promises in the iterable is settled.

Summary

This chapter introduced all the basic concepts of Promises. Keep these three points in mind:

§ A promise is a placeholder for a value that is usually the result of an asynchronous operation.

§ A promise has three states: pending, fulfilled, and rejected.

§ After a promise is fulfilled or rejected, its state and value can never be changed.

At this point you have walked through a number of examples that demonstrate the basic ways a promise is used and you are ready to run sequential asynchronous steps in your own code using promise chains. You should also be comfortable using APIs that return promises for asynchronous work.

One example of promises in the wild is in the CSS Font Load Events spec, which provides a FontFaceSet.load() function that returns a promise for loading fonts into the browser. Consider how you could use this function to only display text once a desired font is loaded in the browser.

Promises can be combined to orchestrate async tasks and structure code in various ways. Although a sequential workflow was provided here, you’ll soon want to use promises in more advanced ways. The next chapter walks through a variety of ways you can use promises in your applications.

1 Chaining then and catch together also allows the catch callback to handle any errors thrown in the callback passed to then. This distinction is explained in Chapter 5.