Promises

A Promise is an abstraction that surrounds the concept of an eventual value. It's easiest to think of a Promise as a simple object that will, at some point, contain a value. A Promise provides an interface via which you can pass callbacks to wait for either the eventually-fulfilled value or an error.

At any given time a Promise will have a certain state:

  • Pending: The Promise is awaiting its resolution (the asynchronous task has not yet completed).
  • Settled: The Promise is no longer pending and has either been fulfilled or rejected:
    • Fulfilled: The Promise has been successful and now has a value
    • Rejected: The Promise has failed with an error

Promises can be constructed via the Promise constructor, by passing a singular function argument (called an executor) that calls either a resolve or reject function to indicate either a settled value or an error, respectively:

const answerToEverything = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(42);
}, 1000);
});

The instantiated Promise has the following methods available so that we can access its changed state (when it moves from pending to either fulfilled or rejected):

  • then(onFulfilled[, onRejected]): This will append a fulfillment callback to the Promise and optionally a rejection callback. It will return a new Promise object, which will resolve to the return value of the called fulfillment or rejection handler, or will resolve as per the original Promise if there is no handler.
  • catch(onRejected): This will append a rejection callback to the Promise and will return a new Promise that will resolve to either the return value of the callback or (if the original Promise succeeds) its fulfillment value.
  • finally(onFinally): This will append a handler to the Promise, which will be called when the Promise is resolved, regardless of whether the resolution is a fulfillment or a rejection.

We can access the eventually resolved value of answerToEverything by passing a callback to its then method:

answerToEverything.then(answer => {
answer; // => 42
});

We can illustrate the exact nature of a Promise by exploring the native Fetch API, supported by most modern browsers:

const promiseOfData = fetch('/some/data?foo=bar');

The fetch function returns a Promise that we assign to our variable, promiseOfData. We can then hook into the request's eventual success (or failure) like so:

const promiseOfData = fetch('/some/data');

promiseOfData.then(
response => {
response; // The "fulfilled" Response
},
error => {
error; // The "rejected" Error
}
);

It may appear as though promises are just a slightly more verbose abstraction than callbacks. Indeed, in the simplest case, you might just pass a fulfillment callback and a rejection callback. This, arguably, does not provide us with anything more useful than the original callback approach. But promises can be so much more than this.

Since a Promise is just a regular object, it can be passed around your program just like any other value, meaning that the eventual resolution of a task no longer needs to be tied to code at the call site of the original task. Additionally, the fact that each then, catchor finally call returns a Promise of its own, we can chain together any number of either synchronous or asynchronous tasks that rely on some original fulfillment.

In the case of fetch(), for example, the fulfilled Response object provides a json() method, which itself completes asynchronously and returns a Promise. Hence, to get the actual JSON data from a given resource, you would have to do the following:

fetch('/data/users')
.then(response => response.json())
.then(jsonDataOfUsers => {
jsonDataOfUsers; // the JSON data that we got from response.json()
});

Chaining together then calls is a popular pattern used to derive a new value from some prior value. Given the response, we wish to compute the JSON, and given the JSON, we may wish to compute something else:

fetch('/data/users')
.then(response => response.json())
.then(users => users.map(user => user.forename))
.then(userForenames => userForenames.sort());

Here, we are using multiple then calls to compute the sorted forenames of our users. There are, in fact, four distinct promises being created here, as foll:

const promiseA = fetch('/data/users');
const promiseB = promiseA.then(response => response.json());
const promiseC = promiseB.then(users => users.map(user => user.forename))
const promiseD = promiseC.then(userForenames => userForenames.sort());

promiseA === promiseB; // => false
promiseB === promiseC; // => false
promiseC === promiseD; // => false

Each Promise will only ever resolve to a single value. Once it's been either fulfilled or rejected, no other value can take its place. But as we see here, we can freely derive a new Promise from an original Promise by simply registering a callback via then, catchor finally. The nature of only resolving once and of returning new derived promises means that we can compose promises together in a number of useful ways. In our example, we could derive two promises from our users data Promise: one that collects the forenames of users and another that collects their surnames:

const users = fetch('/data/users').then(r => r.json());
const forenames = users.then(users => users.map(user => user.forename));
const surnames = users.then(users => users.map(user => user.surname));

We can then freely pass around these forenames and surnames promises, and any consuming code can do what it wants with them. For example, we may have a DOM element that we'd like to populate with the forenames when they are eventually available:

function createForenamesComponent(forenamesPromise) {

const div = document.createElement('div');

function render(forenames) {
div.textContent = forenames ? forenames.join(', ') : 'Loading...';
}

render(null); // Initial render

forenamesPromise.then(forenames => {
// When we receive the forenames we want to render them:
render(forenames);
});

return div;
}

This createForenamesComponent function accepts the forenames Promise as an argument and then returns a <div> element. As you can see, we have called render() initially with null, which populates the DIV element with the "loading..." text. Once the Promise is fulfilled, we then re-render with the newly populated forenames.

The ability to pass around promises in this manner makes them far more flexible than callbacks, and similar in spirit to an object that implements an Events API. However, with all of these mechanisms, it is necessary to create and pass around functions so that you can listen for future Events and then act on them. If you have a significant amount of asynchronous logic to express, this can be a real struggle. The control flow of a program littered with callbacks, Events, and promises can be unclear, even to those well accustomed to a particular code base. Even a small number of independently asynchronous Events can create a large variety of states throughout your application. A programmer can become very confused, as a result; the confusion relates to what is happening when

The state of your program is determined at runtime. When a value or piece of data changes, no matter how small, it will be considered a change of state. State is typically expressed in terms of outputs from the program, such as a GUI or a CLI can be also be held internally and manifest in a later observed output.

To avoid confusion, it's best to implement any timing-related code as transparently as possible, so that there is no room for misunderstanding. The following is an example of code that may lead to misunderstanding:

userInfoLoader.init();

appStartup().then(() => {
const userID = userInfoLoader.data.id;
const userName = userInfoLoader.data.name;
renderApplication(userID, userName);
});

This code seems to assume that the Promise returned by appStartup() will always fulfill after userInfoLoader has completed its work. Perhaps the author of this code happens to know that the appStartup() logic will always complete after userInfoLoader. Perhaps that is a certainty. But for us, reading this code for the first time, we have no confidence that userInfoLoader.data will be populated by the time appStartup() is fulfilled. It would be better to make the timing more transparent by, for example, returning a Promise from userInfoLoader.init() and then carrying out appStartup() on the explicit fulfillment of that Promise:

userInfoLoader.init()
.then(() => appStartup())
.then(() => {
const userID = userInfoLoader.data.id;
const userName = userInfoLoader.data.name;
renderApplication(userID, userName);
});

Here, we are arranging our code so that it is obvious what actions are dependent on what other actions and in what order the actions will occur. Using promises by themselves, just like any other asynchronous control flow abstraction, does not guarantee that your code will be easily comprehensible. It's important to always consider the perspective of your fellow programmers and the temporal assumptions that they'll make. Next, we will explore a newer addition to JavaScript that gives us native linguistic support for asynchronous code: you'll see how these additions enable us to write asynchronous code that is clearer in terms of what is happening when.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.98.13