11
Promises and Async Functions

WHAT'S IN THIS CHAPTER?

  • Introduction to Asynchronous Programming
  • Promises
  • Async Functions

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e on the Download Code tab.

In ECMAScript editions beginning with ES6, support and tooling for asynchronous behavior has undergone a renaissance. ECMAScript 6 introduces a formal Promise reference type, allowing for elegant definition and organization of asynchronous behavior. Later editions also extended the language to support asynchronous functions with the async and await keywords.

INTRODUCTION TO ASYNCHRONOUS PROGRAMMING

The duality between synchronous and asynchronous behavior is a fundamental concept in computer science—especially in a single-threaded event loop model such as JavaScript. Asynchronous behavior is borne out of the need to optimize for higher computational throughput in the face of high-latency operations. If it is feasible to run other instructions while a computation is completing and still maintain a stable system, then it is pragmatic to do so.

Importantly, an asynchronous operation is not necessarily a computationally intensive or high-latency operation. It can be used anywhere it doesn't make sense to block a thread of execution to wait for the asynchronous behavior to occur.

Synchronous vs. Asynchronous JavaScript

Synchronous behavior is analogous to sequential processor instructions in memory. Each instruction is executed strictly in the order in which it appears, and each is also capable of immediately retrieving information that is stored locally within the system (for example: in a processor register or in system memory). As a result, it is easy to reason about the program state (for example, the value of a variable) at any given point in code.

A trivial example of this would be performing a simple arithmetic operation:

let x = 3; 
x = x + 4;

At each step in this program, it is possible to reason about the state of the program because execution will not proceed until the previous instruction is completed. When the last instruction completes, the computed value of x is immediately available for use.

This JavaScript snippet is easy to reason about because it is not difficult to anticipate what low-level instructions this will be compiled to (from JavaScript to x86, for example). Presumably, the operating system will allocate some memory for a floating point number on the stack, perform an arithmetic operation on that value, and write the result to that allocated memory. All of these instructions exist serially inside a single thread of execution. At each point in the compiled low-level program, you are well-equipped to assert what can and cannot be known about the state of the system.

Conversely, asynchronous behavior is analogous to interrupts, where an entity external to the current process is able to trigger code execution. An asynchronous operation is often required because it is infeasible to force the process to wait a long time for an operation to complete (which is the case with a synchronous operation). This long wait might occur because the code is accessing a high-latency resource, such as sending a request to a remote server and awaiting a response.

A trivial JavaScript example of this would be performing an arithmetic operation inside a timeout:

let x = 3; 
setTimeout(() => x = x + 4, 1000);

This program eventually performs the same work as the synchronous one—adding two numbers together—but this thread of execution cannot know exactly when the value of x will change because that depends on when the callback is dequeued from the message queue and executed.

This code is not as easy to reason about. Although the low-level instructions used in this example ultimately do the same work as the previous example, the second chunk of instructions (the addition operation and assignment) are triggered by a system timer, which will generate an interrupt to enqueue execution. Precisely when this interrupt will be triggered is a black box to the JavaScript runtime, so it effectively cannot be known exactly when the interrupt will occur (although it is guaranteed to be after the current thread of synchronous execution completes, since the callback will not yet have had an opportunity to be dequeued and executed). Nevertheless, you are generally unable to assert when the system state will change after the callback is scheduled.

For the value of x to become useful, this asynchronously executed function would need to signal to the rest of the program that it has updated the value of x. However, if the program does not need this value, then it is free to proceed and do other work instead of waiting for the result.

Designing a system to know when the value of x can be read is surprisingly tricky. Implementations of such a system within the JavaScript have undergone several iterations.

Legacy Asynchronous Programming Patterns

Asynchronous behavior has long been an important but ugly cornerstone of JavaScript. In early versions of the language, an asynchronous operation only supported definition of a callback function to indicate that the asynchronous operation had completed. Serializing asynchronous behavior was a common problem, usually solved by a codebase full of nested callback functions—colloquially referred to as “callback hell.”

Suppose you were working with the following asynchronous function, which uses setTimeout to perform some behavior after one second:

function double(value) {
 setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
}

double(3);
// 6 (printed after roughly 1000ms)

There's nothing mysterious occurring here, but it is important to understand exactly why this is an asynchronous function. setTimeout allows for the definition of a callback that is scheduled to execute after a specified amount of time. After 1000ms, the JavaScript runtime will schedule the callback for execution by pushing it onto JavaScript's message queue. This callback is dequeued and executed in a manner that is totally invisible to JavaScript code. What's more, the double() function exits immediately after the setTimeout scheduling operation is successful.

Returning Asynchronous Values

Suppose the setTimeout operation returned a useful value. What's the best way to transport the value back to where it is needed? The widely accepted strategy is to provide a callback to the asynchronous operation, where the callback contained the code that needs access to the calculated value (provided as a parameter). This looks like the following:

function double(value, callback) {
 setTimeout(() => callback(value * 2), 1000);
}

double(3, (x) => console.log(`I was given: ${x}`));
// I was given: 6 (printed after roughly 1000ms)

Here, the setTimeout invocation is instructed to push a function onto the message queue after 1000ms have elapsed. This function will be dequeued and asynchronously evaluated by the runtime. The callback function and its parameters are still available in the asynchronous execution through a function closure.

Handling Failure

The possibility of failure needs to be incorporated into this callback model as well, so this typically took the form of a success and failure callback:

function double(value, success, failure) {
 setTimeout(() => {
  try {
   if (typeof value !== 'number') {
    throw 'Must provide number as first argument';
   }
   success(2 * value);
  } catch (e) {
   failure(e);
  }
 }, 1000);
}

const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback); 
double('b', successCallback, failureCallback);

// Success: 6 (printed after roughly 1000ms) 
// Failure: Must provide number as first argument (printed after roughly 1000ms)

This format is already undesirable, as the callbacks must be defined when the asynchronous operation is initialized. The value returned from the asynchronous function is transient, and so only callbacks that are prepared to accept this transient value as a parameter are capable of accessing it.

Nesting Asynchronous Callbacks

The callback situation is further complicated when access to the asynchronous values have dependencies on other asynchronous values. In the world of callbacks, this necessitates nesting the callbacks:

function double(value, success, failure) {
 setTimeout(() => {
  try {
   if (typeof value !== 'number') {
    throw 'Must provide number as first argument';
   }
   success(2 * value);
  } catch (e) {
   failure(e);
  }
 }, 1000);
}

const successCallback = (x) => {
 double(x, (y) => console.log(`Success: ${y}`));
};
const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback);

// Success: 12 (printed after roughly 1000ms)

It should come as no surprise that this callback strategy does not scale well as code complexity grows. The “callback hell” colloquialism is well-deserved, as JavaScript codebases that were afflicted with such a structure became nearly unmaintainable.

PROMISES

A “promise” is a surrogate entity that acts as a stand-in for a result that does not yet exist. The term “promise” was first proposed by Daniel Friedman and David Wise in their 1976 paper, The Impact of Applicative Programming on Multiprocessing, but the conceptual behavior of a promise would not be formalized until a decade later by Barbara Liskov and Liuba Shrira in their 1988 paper, Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems. Contemporary computer scientists described similar concepts such as an “eventual,” “future,” “delay,” or “deferred”; all of these described in one form or another a programming tool for synchronizing program execution.

The Promises/A+ Specification

Early forms of promises appeared in jQuery and Dojo's Deferred API, and in 2010, growing popularity led to the Promises/A specification inside the CommonJS project. Third-party JavaScript promise libraries such as Q and Bluebird continued to gain adoption, yet each implementation was slightly different. To address the rifts in the promise space, in 2012 the Promises/A+ organization forked the CommonJS "Promises/A" proposal and created the eponymous Promises/A+ Promise Specification (https://promisesaplus.com/). This specification would eventually govern how promises were implemented in the ECMAScript 6 specification.

ECMAScript 6 introduced a first-class implementation of a Promises/A+–compliant Promise type. In the time since its introduction, Promises have enjoyed an overwhelmingly robust rate of adoption. All modern browsers fully support the ES6 promise type, and multiple browser APIs such as fetch() and the battery API use it exclusively.

Promise Basics

As of ECMAScript 6, Promise is a supported reference type and can be instantiated with the new operator. Doing so requires passing an executor function parameter (covered in an upcoming section), which here is an empty function object to please the interpreter:

let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>

If an executor function is not provided, a SyntaxError will be thrown.

The Promise State Machine

When passing a promise instance to console.log, the console output (which may vary between browsers) indicates that this promise instance is pending. As mentioned previously, a promise is a stateful object that can exist in one of three states:

  • Pending
  • Fulfilled (sometimes also referred to as resolved)
  • Rejected

A pending state is the initial state a promise begins in. From a pending state, a promise can become settled by transitioning to a fulfilled state to indicate success, or a rejected state to indicate failure. This transition to a settled state is irreversible; once a transition to either fulfilled or rejected occurs, the state of the promise can never change. Furthermore, it is not guaranteed that a promise will ever leave the pending state. Therefore, well-structured code should behave properly if the promise successfully resolves, if the promise rejects, or if it never exits the pending state.

Importantly, the state of a promise is private and cannot be directly inspected in JavaScript. The reason for this is primarily to prevent synchronous programmatic handling of a promise object based on its state when it is read. Furthermore, the state of a promise cannot be mutated by external JavaScript. This is for the same reason the state cannot be read: The promise intentionally encapsulates a block of asynchronous behavior, and external code performing synchronous definition of its state would be antithetical to its purpose.

Resolved Values, Rejection Reasons, and Utility of Promises

There are two primary reasons the Promise construct is useful. The first is to abstractly represent a block of asynchronous execution. The state of the promise is indicative of whether or not the promise has yet to complete execution. The pending state indicates that execution has not yet begun or is still in progress. The fulfilled state is a nonspecific indicator that the execution has completed successfully. The rejected state is a nonspecific indicator that the execution did not complete successfully.

In some cases, the internal state machine is all the utility a promise needs to provide: the mere knowledge that a piece of asynchronous code has completed is sufficient for informing program flow. For example, suppose a promise is dispatching an HTTP request to a server. The request returning with a status of 200–299 might be sufficient to transition the promise state to fulfilled. Similarly, the request returning with a status that is not 200-299 would transition the promise state to rejected.

In other cases, the asynchronous execution that the promise is wrapping is actually generating a value, and the program flow will expect this value to be available when the promise changes state. Alternately, if the promise rejects, the program flow will expect the reason for rejection when the promise changes state. For example, suppose a promise is dispatching an HTTP request to a server and expecting it to return JSON. The request returning with a status of 200–299 might be sufficient to transition the promise to fulfilled, and the JSON string will be available inside the promise. Similarly, the request returning with a status that is not 200–299 would transition the promise state to rejected, and the reason for rejection might be an Error object containing the text accompanying the HTTP status code.

To support these two use cases, every promise that transitions to a fulfilled state has a private internal value. Similarly, every promise that transitions to a rejected state has a private internal reason. Both value and reason are an immutable reference to a primitive or object. Both are optional and will default to undefined. Asynchronous code that is scheduled to execute after a promise reaches a certain settled state is always provided with the value or reason.

Controlling Promise State with the Executor

Because the state of a promise is private, it can only be manipulated internally. This internal manipulation is performed inside the promise's executor function. The executor function has two primary duties: initializing the asynchronous behavior of the promise, and controlling any eventual state transition. Control of the state transition is accomplished by invoking one of its two function parameters, typically named resolve and reject. Invoking resolve will change the state to fulfilled; invoking reject will change the state to rejected. Invoking rejected()will also throw an error (this error behavior is covered more later).

let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>

let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)

In the preceding example, there isn't really any asynchronous behavior occurring because the state of each promise is already changed by the time the executor function exits. Importantly, the executor function will execute synchronously, as it acts as the initializer for the promise. This order of execution is demonstrated here:

new Promise(() => setTimeout(console.log, 0, 'executor'));
setTimeout(console.log, 0, 'promise initialized');

// executor
// promise initialized

You can delay the state transition by adding a setTimeout:

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));

// When this console.log executes, the timeout callback has not yet executed:
setTimeout(console.log, 0, p); // Promise <pending>

Once either resolve or reject is invoked, the state transition cannot be undone. Attempts to further mutate the state will silently be ignored. This is demonstrated here:

let p = new Promise((resolve, reject) => {
 resolve();
 reject(); // No effect
});

setTimeout(console.log, 0, p); // Promise <resolved>

You can avoid promises getting stuck in a pending state by adding timed exit behavior. For example, you can set a timeout to reject the promise after 10 seconds:

let p = new Promise((resolve, reject) => {
 setTimeout(reject, 10000); // After 10 seconds, invoke reject()

 // Do executor things
});

setTimeout(console.log, 0, p);   // Promise <pending>
setTimeout(console.log, 11000, p); // Check state after 11 seconds

// (After 10 seconds) Uncaught error
// (After 11 seconds) Promise <rejected>

Because a promise can only change state a single time, this timeout behavior allows you to safely set a maximum on the amount of time a promise can remain in the pending state. If the code inside the executor were to resolve or reject prior to the timeout, the timeout handler's attempt to reject the promise will be silently ignored.

Promise Casting with Promise.resolve()

A promise does not necessarily need to begin in a pending state and utilize an executor function to reach a settled state. It is possible to instantiate a promise in the "resolved" state by invoking the Promise.resolve() static method. The following two promise instantiations are effectively equivalent:

let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();

The value of this resolved promise will become the first argument passed to Promise.resolve(). This effectively allows you to "cast" any value into a promise:

setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined

setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3

// Additional arguments are ignored
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4

Perhaps the most important aspect of this static method is its ability to act as a passthrough when the argument is already a promise. As a result, Promise.resolve() is an idempotent method, as demonstrated here:

let p = Promise.resolve(7);

setTimeout(console.log, 0, p === Promise.resolve(p));
// true

setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

This idempotence will respect the state of the promise passed to it:

let p = new Promise(() => {});

setTimeout(console.log, 0, p);          // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>

setTimeout(console.log, 0, p === Promise.resolve(p)); // true

Beware that this static method will happily wrap any non-promise, including an error object, as a resolved promise, which might lead to unintended behavior:

let p = Promise.resolve(new Error('foo'));

setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo

Promise Rejection with Promise.reject()

Similar in concept to Promise.resolve(), Promise.reject() instantiates a rejected promise and throws an asynchronous error (which will not be caught by try/catch and can only be caught by a rejection handler). The following two promise instantiations are effectively equivalent:

let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();

The ‘reason' field of this resolved promise will be the first argument passed to Promise.reject(). This will also be the error passed to the reject handler:

let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3

p.then(null, (e) => setTimeout(console.log, 0, e)); // 3

Importantly, Promise.reject() does not mirror the behavior of Promise.resolve() with respect to idempotence. If passed a promise object, it will happily use that promise as the ‘reason' field of the rejected promise:

setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>

Synchronous/Asynchronous Execution Duality

Much of the design of the Promise construct is to engender a totally separate mode of computation in JavaScript. This is neatly encapsulated in the following example, which throws errors in two different ways:

try {
  throw new Error('foo');
} catch(e) {
  console.log(e); // Error: foo
}

try {
  Promise.reject(new Error('bar'));
} catch(e) {
  console.log(e);
}
// Uncaught (in promise) Error: bar

The first try/catch block throws an error and then proceeds to catch it, but the second try/catch block throws an error that is not caught. This might seem counterintuitive because the code appears to be synchronously creating a rejected promise instance, which then throws an error upon rejection. However, the reason the second promise is not caught is that the code is not attempting to catch the error in the appropriate "asynchronous mode." Such behavior underscores how promises actually behave: They are synchronous objects—used inside a synchronous mode of execution—acting as a bridge to an asynchronous mode of execution.

In the preceding example, the error from the rejected promise is not being thrown inside the synchronous execution thread, but instead from the browser's asynchronous message queue execution. Therefore, the encapsulating try/catch block will not suffice to catch this error. Once code starts to execute in this asynchronous mode, the only way to interact with it is to use asynchronous mode constructs—more specifically, promise methods.

Promise Instance Methods

The methods exposed on a promise instance serve to bridge the gap between the synchronous external code path and the asynchronous internal code path. These methods can be used to access data returned from an asynchronous operation, handle success and failure outcomes of the promise, serially evaluate promises, or add functions that only execute once the promise enters a terminal state.

Implementing the Thenable Interface

For the purposes of ECMAScript asynchronous constructs, any object that exposes a then() method is considered to implement the Thenable interface. The following is an example of the simplest possible class that implements this interface:

class MyThenable {
 then() {}
}

The ECMAScript Promise type implements the Thenable interface. This simplistic interface is not to be confused with other interfaces or type definitions in packages like TypeScript, which lay out a much more specific form of a Thenable interface.

Promise.prototype.then()

The method Promise.prototype.then() is the primary method that is used to attach handlers to a promise instance. The then() method accepts up to two arguments: an optional onResolved handler function, and an optional onRejected handler function. Each will execute only when the promise upon which they are defined reaches its respective "fulfilled" or "rejected" state.

function onResolved(id) {
 setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
 setTimeout(console.log, 0, id, 'rejected');
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));

p1.then(() => onResolved('p1'), 
        () => onRejected('p1'));
p2.then(() => onResolved('p2'), 
        () => onRejected('p2'));

// (after 3s)
// p1 resolved
// p2 rejected

Because a promise can only transition to a final state a single time, you are guaranteed that execution of these handlers is mutually exclusive.

As described earlier, both handler arguments are completely optional. Any non-function type provided as an argument to then() will be silently ignored. If you wish to explicitly provide only an onRejected handler, providing undefined as the onResolved argument is the canonical choice. This allows you to avoid creating a temporary object in memory just to be ignored by the interpreter, and it will also please type systems that expect an optional function object as an argument.

function onResolved(id) {
 setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
 setTimeout(console.log, 0, id, 'rejected');
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));

// Non-function handlers are silently ignored, not recommended
p1.then('gobbeltygook');

// Canonical form of explicit onResolved handler skipping
p2.then(null, () => onRejected('p2'));

// p2 rejected (after 3s) 

The Promise.prototype.then() method returns a new promise instance:

let p1 = new Promise(() => {});
let p2 = p1.then();
setTimeout(console.log, 0, p1);        // Promise <pending>
setTimeout(console.log, 0, p2);        // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false

This new promise instance is derived from the return value of the onResolved handler. The return value of the handler is wrapped in Promise.resolve() to generate a new promise. If no handler function is provided, the method acts as a passthrough for the initial promise's resolved value. If there is no explicit return statement, the default return value is undefined and wrapped in a Promise.resolve().

let p1 = Promise.resolve('foo');

// Calling then() with no handler function acts as a passthrough
let p2 = p1.then();

setTimeout(console.log, 0, p2); // Promise <resolved>: foo

// These are equivalent
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined 

Explicit return values are wrapped in Promise.resolve():



// These are equivalent:
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

// Promise.resolve() preserves the returned promise
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined 

Throwing an exception will return a rejected promise:



let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz

setTimeout(console.log, 0, p10); // Promise <rejected> baz 

Importantly, returning an error will not trigger the same rejection behavior, and will instead wrap the error object in a resolved promise:



let p11 = p1.then(() => Error('qux'));

setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux 

The onRejected handler behaves in the same way: values returned from the onRejected handler are wrapped in Promise.resolve(). This might seem counterintuitive at first, but the onRejected handler is doing its job to catch an asynchronous error. Therefore, this rejection handler completing execution without throwing an additional error should be considered expected promise behavior and therefore return a resolved promise.

The following code snippet is the Promise.reject() analog of the previous examples using Promise.resolve():

let p1 = Promise.reject('foo');

// Calling then() with no handler function acts as a passthrough
let p2 = p1.then();
// Uncaught (in promise) foo

setTimeout(console.log, 0, p2); // Promise <rejected>: foo

// These are equivalent
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());

setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined



// These are equivalent
let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));

setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

// Promise.resolve() preserves the returned promise
let p8 = p1.then(null, () => new Promise(() => {}));
let p9 = p1.then(null, () => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined



let p10 = p1.then(null, () => { throw 'baz'; });
// Uncaught (in promise) baz

setTimeout(console.log, 0, p10); // Promise <rejected>: baz



let p11 = p1.then(null, () => Error('qux'));

setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux 

Promise.prototype.catch()

The Promise.prototype.catch() method can be used to attach only a reject handler to a promise. It only takes a single argument, the onRejected handler function. The method is no more than syntactical sugar, and is no different than using Promise.prototype.then(null, onRejected).

The following code demonstrates this equivalence:

let p = Promise.reject();
let onRejected = function(e) {
 setTimeout(console.log, 0, 'rejected');
};

// These two reject handlers behave identically:
p.then(null, onRejected); // rejected
p.catch(onRejected);      // rejected 

The Promise.prototype.catch() method returns a new promise instance:

let p1 = new Promise(() => {});
let p2 = p1.catch();
setTimeout(console.log, 0, p1);        // Promise <pending>
setTimeout(console.log, 0, p2);        // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false

With respect to creation of the new promise instance, Promise.prototype.catch() behaves identically to the onRejected handler of Promise.prototype.then().

Promise.prototype.finally()

The Promise.protoype.finally() method can be used to attach an onFinally handler, which executes when the promise reaches either a resolved or a rejected state. This is useful for avoiding code duplication between onResolved and onRejected handlers. Importantly, the handler does not have any way of determining if the promise was resolved or rejected, so this method is designed to be used for things like cleanup.

let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
 setTimeout(console.log, 0, 'Finally!')
}

p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally

The Promise.prototype.finally() method returns a new promise instance:

let p1 = new Promise(() => {});
let p2 = p1.finally();
setTimeout(console.log, 0, p1);        // Promise <pending>
setTimeout(console.log, 0, p2);        // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false

This new promise instance is derived in a different manner than then() or catch(). Because onFinally is intended to be a state-agnostic method, in most cases it will behave as a passthrough for the parent promise. This is true for both the resolved and rejected states.

let p1 = Promise.resolve('foo');

// These all act as a passthrough
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));

setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo 

The only exceptions to this are when it returns a pending promise, or an error is thrown (via an explicit throw or returning a rejected promise). In these cases, the corresponding promise is returned (pending or rejected), as shown here:



// Promise.resolve() preserves the returned promise
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined

let p11 = p1.finally(() => { throw 'baz';});
// Uncaught (in promise) baz

setTimeout(console.log, 0, p11); // Promise <rejected>: baz

Returning a pending promise is an unusual case, as once the promise resolves, the new promise will still behave as a passthrough for the initial promise:

let p1 = Promise.resolve('foo');

// The resolved value is ignored
let p2 = p1.finally(
 () => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100)));

setTimeout(console.log, 0, p2); // Promise <pending>

setTimeout(() => setTimeout(console.log, 0, p2), 200);

// After 200ms:
// Promise <resolved>: foo 

Non-Reentrant Promise Methods

When a promise reaches a settled state, execution of handlers associated with that state are merely scheduled rather than immediately executed. Synchronous code following the attachment of the handler is guaranteed to execute before the handler is invoked. This remains true even if the promise already exists in the state the newly attached handler is associated with. This property, called non-reentrancy, is guaranteed by the JavaScript runtime. The following simple example demonstrates this property:

// Create a resolved promise
let p = Promise.resolve();

// Attach a handler to the resolved state.
// Intuitively, this would execute as soon as possible since p is already resolved.
p.then(() => console.log('onResolved handler'));

// Synchronously log to indicate that then() has returned
console.log('then() returns');

// Actual output:
// then() returns
// onResolved handler

In this example, calling then() on a resolved promise will push the onResolved handler onto the message queue. This handler will not be executed until it is dequeued after the current thread of execution completes. Therefore, synchronous code immediately following then() is guaranteed to execute prior to the handler.

The inverse of this scenario yields the same result. If handlers are already attached to a promise that later synchronously changes state, the handler execution is non-reentrant upon that state change. The following example demonstrates how, even with an onResolved handler already attached, synchronously invoking resolve() will still exhibit non-reentrant behavior:

let synchronousResolve;

// Create a promise and capture the resolve function in a local variable
let p = new Promise((resolve) => {
 synchronousResolve = function() {
  console.log('1: invoking resolve()');
  resolve();
  console.log('2: resolve() returns');
 };
});

p.then(() => console.log('4: then() handler executes'));

synchronousResolve();
console.log('3: synchronousResolve() returns');

// Actual output:
// 1: invoking resolve()
// 2: resolve() returns
// 3: synchronousResolve() returns
// 4: then() handler executes 

In this example, even though the promise state changes synchronously with handlers attached to that state, the handler execution will still not occur until dequeued from the runtime's message queue.

Non-reentrancy is guaranteed for both onResolved and onRejected handlers, catch() handlers, and finally() handlers. Each of these is demonstrated in this example:

let p1 = Promise.resolve();
p1.then(() => console.log('p1.then() onResolved'));
console.log('p1.then() returns');

let p2 = Promise.reject();
p2.then(null, () => console.log('p2.then() onRejected'));
console.log('p2.then() returns');

let p3 = Promise.reject();
p3.catch(() => console.log('p3.catch() onRejected'));
console.log('p3.catch() returns');

let p4 = Promise.resolve();
p4.finally(() => console.log('p4.finally() onFinally'));

console.log('p4.finally() returns'); 

// p1.then() returns
// p2.then() returns
// p3.catch() returns
// p4.finally() returns
// p1.then() onResolved
// p2.then() onRejected
// p3.catch() onRejected
// p4.finally() onFinally 

Sibling Handler Order of Execution

If multiple handlers are attached to a promise, when the promise transitions to a settled state, the associated handlers will execute in the order in which they were attached. This is true for then(), catch(), and finally():

let p1 = Promise.resolve();
let p2 = Promise.reject();

p1.then(() => setTimeout(console.log, 0, 1));
p1.then(() => setTimeout(console.log, 0, 2));
// 1
// 2

p2.then(null, () => setTimeout(console.log, 0, 3));
p2.then(null, () => setTimeout(console.log, 0, 4));
// 3
// 4

p2.catch(() => setTimeout(console.log, 0, 5));
p2.catch(() => setTimeout(console.log, 0, 6));
// 5
// 6

p1.finally(() => setTimeout(console.log, 0, 7));
p1.finally(() => setTimeout(console.log, 0, 8));
// 7
// 8 

Resolved Value and Rejected Reason Passing

When reaching a settled state, a promise will provide its resolved value (if it is fulfilled) or its rejection reason (if it is rejected) to any handlers that are attached to that state. This is especially useful in cases where successive blocks of serial computation are required. For example, if the JSON response of one network request is required to perform a second network request, the response from the first request can be passed as the resolved value to the onResolved handler. Alternately, a failed network request can pass the HTTP status code to the onRejected handler.

Resolved values and rejected reasons are assigned from inside the executor as the first argument to the resolve() or reject() functions. These values are provided to their respective onResolved or onRejected handler as the sole parameter. This handoff is demonstrated here:

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value));   // foo

let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar

Promise.resolve() and Promise.reject() accept the value/reason argument when calling the static method. The onResolved and onRejected handlers are provided the value or reason in the same way as if it were passed from the executor:

let p1 = Promise.resolve('foo');
p1.then((value) => console.log(value));   // foo

let p2 = Promise.reject('bar');
p2.catch((reason) => console.log(reason)); // bar

Rejecting Promises and Rejection Error Handling

Rejecting a promise is analogous to a throw expression in that they both represent a program state that should force a discontinuation of any subsequent operations. Throwing an error inside a promise executor or handler will cause it to reject; the corresponding error object will be the rejection reason. Therefore, these promises all will reject with an error object:

let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));

setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo


// Also throws four uncaught errors

Promises can be rejected with any value including undefined, but it is strongly recommended that you consistently use error object. The primary reason for this is that constructing an error object allows the browser to capture the stack trace inside the error object, which is immensely useful in debugging. For example, the stack trace for the three errors in the preceding code should appear something like the following:

Uncaught (in promise) Error: foo
  at Promise (test.html:5)
  at new Promise (<anonymous>)
  at test.html:5
Uncaught (in promise) Error: foo
  at Promise (test.html:6)
  at new Promise (<anonymous>)
  at test.html:6
Uncaught (in promise) Error: foo
  at test.html:8
Uncaught (in promise) Error: foo
  at Promise.resolve.then (test.html:7)

All errors are asynchronously thrown and unhandled, and the stack trace captured by the error objects show the path that the error object took. Also note here the order of errors: The Promise.resolve().then() error executes last because it requires an additional entry on the runtime's message queue because it is creating one additional promise prior to ultimately throwing the uncaught error.

This example also uncovers an interesting side effect of asynchronous errors. Normally, when throwing an error using the throw keyword, the JavaScript runtime's error handling behavior will decline to execute any instructions following the thrown error.

throw Error('foo');
console.log('bar'); // This will never print

// Uncaught Error: foo

However, when an error is thrown inside a promise, because the error is actually being thrown asynchronously from the message queue, it will not prevent the runtime from continuing to execute synchronous instructions:

Promise.reject(Error('foo'));
console.log('bar');
// bar

// Uncaught (in promise) Error: foo

As demonstrated earlier in this chapter with Promise.reject(), an asynchronous error can be caught only with an asynchronous onRejection handler.

// Correct
Promise.reject(Error('foo')).catch((e) => {});

// Incorrect
try {
 Promise.reject(Error('foo'));
} catch(e) {}

This does not apply to catching the error while still inside the executor, where try/catch will still suffice to catch the error before it rejects the promise:

let p = new Promise((resolve, reject) => {
 try {
  throw Error('foo');
 } catch(e) {}

 resolve('bar');
});

setTimeout(console.log, 0, p); // Promise <resolved>: bar

The onRejected handler for then() and catch() is analogous to the semantics of try/catch in that catching an error should effectively neutralize it and allow for normal computation to continue. Therefore, it should make sense that an onRejected handler tasked with catching asynchronous error will in fact return a resolved promise. The synchronous/synchronous comparison are demonstrated in the following example:

console.log('begin synchronous execution');
try {
 throw Error('foo');
} catch(e) {
 console.log('caught error', e);
}
console.log('continue synchronous execution');

// begin synchronous execution
// caught error Error: foo
// continue synchronous execution



new Promise((resolve, reject) => {
 console.log('begin asynchronous execution');
 reject(Error('bar'));
}).catch((e) => {
 console.log('caught error', e);
}).then(() => {
 console.log('continue asynchronous execution');
});

// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution 

Promise Chaining and Composition

Combining multiple promises together enables some powerful code patterns. Such behavior is possible in two primary ways: promise chaining, which involves strictly sequencing multiple promises, and promise composition, which involves combining multiple promises into a single promise.

Promise Chaining

One of the more useful aspects of ECMAScript promises is their ability to be strictly sequenced. This is enabled through the Promise API's structure: Each of the promise instance's methods—then(), catch(), and finally()—returns a separate promise instance, which in turn can have another instance method called upon it. Successively invoking methods in such a manner is referred to as “promise chaining.” The following is a trivial example of this:

let p = new Promise((resolve, reject) => { 
 console.log('first');
 resolve();
});
p.then(() => console.log('second'))
 .then(() => console.log('third'))
 .then(() => console.log('fourth'));

// first
// second
// third
// fourth

This implementation is ultimately executing chained synchronous tasks. Because of this, the work performed isn't very useful or interesting because it is approximately the same as successively invoking four functions in sequence:

(() => console.log('first'))();
(() => console.log('second'))();
(() => console.log('third'))();
(() => console.log('fourth'))();

To chain asynchronous tasks, this example can be retooled so that each executor returns a promise instance. Because each successive promise will await the resolution of its predecessor, such a strategy can be used to serialize asynchronous tasks. For example, this can be used to execute multiple promises in series that resolve after a timeout:

let p1 = new Promise((resolve, reject) => {
 console.log('p1 executor');
 setTimeout(resolve, 1000);
});

p1.then(() => new Promise((resolve, reject) => {
  console.log('p2 executor');
  setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
  console.log('p3 executor');
  setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
  console.log('p4 executor');
  setTimeout(resolve, 1000);
 }));

// p1 executor (after 1s)
// p2 executor (after 2s)
// p3 executor (after 3s)
// p4 executor (after 4s) 

Combining the promise generation into a single factory function yields the following:

function delayedResolve(str) {
 return new Promise((resolve, reject) => {
  console.log(str);
  setTimeout(resolve, 1000);
 });
}

delayedResolve('p1 executor')
 .then(() => delayedResolve('p2 executor'))
 .then(() => delayedResolve('p3 executor'))
 .then(() => delayedResolve('p4 executor')) 

// p1 executor (after 1s)
// p2 executor (after 2s)
// p3 executor (after 3s)
// p4 executor (after 4s) 

Each successive handler waits for its predecessor to resolve, instantiates a new promise instance, and returns it. Such a structure is able to neatly serialize asynchronous code without forcing the use of callbacks. Without the use of promises, the preceding code would look something like this:

function delayedExecute(str, callback = null) {
 setTimeout(() => {
  console.log(str);
  callback && callback();
 }, 1000)
}

delayedExecute('p1 callback', () => {
 delayedExecute('p2 callback', () => {
  delayedExecute('p3 callback', () => {
   delayedExecute('p4 callback');
  });
 });
});

// p1 callback (after 1s)
// p2 callback (after 2s)
// p3 callback (after 3s)
// p4 callback (after 4s)

The observant developer will note that this effectively reintroduces the callback hell that promises are designed to circumvent.

Because then(), catch(), and finally() all return a promise, chaining them together is straightforward. The following example incorporates all three:

let p = new Promise((resolve, reject) => {
 console.log('initial promise rejects');
 reject();
});

p.catch(() => console.log('reject handler'))
 .then(() => console.log('resolve handler'))
 .finally(() => console.log('finally handler'));
 
// initial promise rejects
// reject handler
// resolve handler
// finally handler

Promise Graphs

Because a single promise can have an arbitrary number of handlers attached, forming directed acyclic graphs of chained promises is also possible. Each promise is a node in the graph, and attaching a handler using an instance method adds a directed vertex. Because each node in the graph will wait for its predecessor to settle, you are guaranteed that the direction of graph vertices will indicate the order of promise execution.

The following is an example of one possible type of promise directed graph, a binary tree:

//   A
//  / 
//  B  C
// /  /
// D E F G

let A = new Promise((resolve, reject) => {
 console.log('A');
 resolve();
});

let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));

B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));

// A
// B
// C
// D
// E
// F
// G

Note here that the order of log statements is a level-order traversal of this binary tree. As discussed in an earlier section, a promise's handlers are expected to execute in the order they were attached. Because the handlers of a promise are eagerly added to the message queue but lazily executed, the result of this behavior is a level-order traversal.

Trees are only one manifestation of a promise graph. Because there is not necessarily a single root promise node, and because multiple promises can be combined into a single promise (using Promise.all() or Promise.race() as discussed in an upcoming section), a directed acyclic graph is the most accurate characterization of the universe of promise chaining possibilities.

Parallel Promise Composition with Promise.all() and Promise.race()

The Promise class provides two static methods that allow you to compose a new promise instance out of several promise instances. The behavior of this composed promise is based on how the promises inside it behave.

Promise.all()

The Promise.all() static method creates an all-or-nothing promise that resolves only once every promise in a collection of promises resolves. The static method accepts an iterable and returns a new promise:

let p1 = Promise.all([
 Promise.resolve(), 
 Promise.resolve()
]);

// Elements in the iterable are coerced into a promise using Promise.resolve()
let p2 = Promise.all([3, 4]); 

// Empty iterable is equivalent to Promise.resolve()
let p3 = Promise.all([]);

// Invalid syntax
let p4 = Promise.all();
// TypeError: cannot read Symbol.iterator of undefined

The composed promise will only resolve once every contained promise is resolved:

let p = Promise.all([
 Promise.resolve(), 
 new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p); // Promise <pending>

p.then(() => setTimeout(console.log, 0, 'all() resolved!'));

// all() resolved! (After ~1000ms)

If at least one contained promise remains pending, the composed promise also will remain pending. If one contained promise rejects, the composed promise will reject:

// Will forever remain pending
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// Single rejection causes rejection of composed promise
let p2 = Promise.all([
 Promise.resolve(), 
 Promise.reject(), 
 Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise <rejected>

// Uncaught (in promise) undefined 

If all promises successfully resolve, the resolved value of the composed promise will be an array of all of the resolved values of the contained promises, in iterator order:

let p = Promise.all([
 Promise.resolve(3), 
 Promise.resolve(), 
 Promise.resolve(4)
]);

p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]

If one of the promises rejects, whichever is the first to reject will set the rejection reason for the composed promise. Subsequent rejections will not affect the rejection reason; however, the normal rejection behavior of those contained promise instances is not affected. Importantly, the composed promise will silently handle the rejection of all contained promises, as demonstrated here:

// Although only the first rejection reason will be provided in
// the rejection handler, the second rejection will be silently
// handled and no error will escape
let p = Promise.all([
 Promise.reject(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3

// No unhandled errors
Promise.race()

The Promise.race() static method creates a promise that will mirror whichever promise inside a collection of promises reaches a resolved or rejected state first. The static method accepts an iterable and returns a new promise:

let p1 = Promise.race([
 Promise.resolve(), 
 Promise.resolve()
]);

// Elements in the iterable are coerced into a promise using Promise.resolve()
let p2 = Promise.race([3, 4]); 

// Empty iterable is equivalent to new Promise(() => {})
let p3 = Promise.race([]);

// Invalid syntax
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined

The Promise.race() method does not give preferential treatment to a resolved or a rejected promise. The composed promise will pass through the status and value/reason of the first settled promise, as shown here:

// Resolve occurs first, reject in timeout ignored
let p1 = Promise.race([
 Promise.resolve(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3

// Reject occurs first, resolve in timeout ignored
let p2 = Promise.race([
 Promise.reject(4),
 new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4

// Iterator order is the tiebreaker for settling order
let p3 = Promise.race([
 Promise.resolve(5),
 Promise.resolve(6),
 Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5

If one of the promises rejects, whichever is the first to reject will set the rejection reason for the composed promise. Subsequent rejections will not affect the rejection reason; however, the normal rejection behavior of those contained promise instances is not affected. As is the case with Promise.all(), the composed promise will silently handle the rejection of all contained promises, as demonstrated here:

// Although only the first rejection reason will be provided in
// the rejection handler, the second rejection will be silently
// handled and no error will escape
let p = Promise.race([
 Promise.reject(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);

p.catch((reason) => setTimeout(console.log, 0, reason)); // 3

// No unhandled errors

Serial Promise Composition

The discussion of promise chaining thus far has focused on serialization of execution and largely ignored a core feature of promises: their ability to asynchronously produce a value and provide it to handlers. Chaining promises together with the intention of each successive promise using the value of its predecessor is a fundamental feature of promises. This is in many ways analogous to function composition, where multiple functions are composed together into a new function, demonstrated here:

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function addTen(x) {
 return addFive(addTwo(addThree(x)));
}

console.log(addTen(7)); // 17

In this example, function composition is used to combine these three functions together to operate on a single value. Similarly, promises can be composed together to progressively consume a value and produce a single promise containing the result. Doing so explicitly would appear as follows:

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function addTen(x) {
 return Promise.resolve(x)
  .then(addTwo)
  .then(addThree)
  .then(addFive);
}

addTen(8).then(console.log); // 18 

This can be fashioned into a more succinct form using Array.prototype.reduce():

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function addTen(x) {
 return [addTwo, addThree, addFive]
   .reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}

addTen(8).then(console.log); // 18

Such a strategy for composing promises can be generalized into a function that can compose any number of functions into a value-passing promise chain. This generalized composition function can be implemented as follows:

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

function compose(…fns) {
 return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}

let addTen = compose(addTwo, addThree, addFive);

addTen(8).then(console.log); // 18

Promise Extensions

The ES6 promise implementation is robust, but as is the case with any piece of software, there will be shortcomings. Two offerings available in some third-party promise implementations but lacking in the formal ECMAScript specification are promise canceling and progress tracking.

Promise Canceling

Often, a promise will be in progress but the program will no longer care about the result. The ability to “cancel” a promise would be useful in such a situation. Some third-party promise libraries such as Bluebird offer such a feature, and even ECMAScript was slated to offer such a feature before it was ultimately withdrawn (https://github.com/tc39/proposal-cancelable-promises). As a result, ES6 promises are considered “eager”: Once the promise's encapsulated function is underway, there is no way to prevent this process from completing.

It is still possible to implement an ad-hoc implementation that is a facsimile of the original design. Such an implementation makes use of a “cancel token,” a concept fleshed out in Kevin Smith's design sketch (https://github.com/zenparsing/es-cancel-token). A generated cancel token provides an interface through which to cancel a promise, as well as a promise hook with which to trigger cancellation behavior and evaluate cancellation state.

A basic implementation of a CancelToken class might appear as follows:

class CancelToken {
 constructor(cancelFn) {
  this.promise = new Promise((resolve, reject) => {
   cancelFn(resolve);
  });
 }
}

This class wraps a promise that exposes the resolve method to a cancelFn parameter. An external entity will then be able to provide a function to the constructor, allowing that entity to control exactly when the token should be canceled. This promise is a public member of the token class, and therefore it is possible to add listeners to the cancellation promise.

A rough example of how this class could be used is shown here:

<button id="start">Start</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
 constructor(cancelFn) {
  this.promise = new Promise((resolve, reject) => {
   cancelFn(() => {
    setTimeout(console.log, 0, "delay cancelled");
    resolve();
   });
  });
 }
}

const startButton = document.querySelector('#start');
const cancelButton = document.querySelector('#cancel');

function cancellableDelayedResolve(delay) {
 setTimeout(console.log, 0, "set delay");
 
 return new Promise((resolve, reject) => {
  const id = setTimeout((() => {
   setTimeout(console.log, 0, "delayed resolve");
   resolve();
  }), delay);
  
  const cancelToken = new CancelToken((cancelCallback) => 
    cancelButton.addEventListener("click", cancelCallback));
  
  cancelToken.promise.then(() => clearTimeout(id));
 });
}

startButton.addEventListener("click", () => cancellableDelayedResolve(1000)); 
</script>

Each click on the Start button begins a timeout and instantiates a new CancelToken instance. The Cancel button is configured so that a click will trigger the token's promise to resolve. Upon resolving, the timeout initially set by the Start button click will be canceled.

Promise Progress Notifications

An in-progress promise might have several discrete “stages” that it will progress through before actually resolving. In some situations, it can be useful to allow a program to watch for a promise to reach these checkpoints. ECMAScript 6 promises do not support this concept, but it is still possible to emulate this behavior by extending a promise.

One potential implementation is to extend the Promise class with a notify() method, as shown here:

class TrackablePromise extends Promise {
 constructor(executor) {
  const notifyHandlers = [];
  
 super((resolve, reject) => {
  return executor(resolve, reject, (status) => {
     notifyHandlers.map((handler) => handler(status));
    }) ;
  });
  
   this.notifyHandlers = notifyHandlers;  
 }
 
 notify(notifyHandler) {
  this.notifyHandlers.push(notifyHandler);
  return this;
 }
}

A TrackablePromise would then be able to use the notify() function inside the executor. A promise instantiation could use this function as follows:

let p = new TrackablePromise((resolve, reject, notify) => {
 function countdown(x) {
  if (x> 0) {
    notify(`${20 * x}% remaining`);
    setTimeout(() => countdown(x - 1), 1000);
  } else {
   resolve();
  }
 }

 countdown(5);
}); 

This promise will recursively set a 1000ms timeout five consecutive times before resolving. Each timeout handler will invoke notify() and pass a status. Providing a notification handler could be done as follows:



let p = new TrackablePromise((resolve, reject, notify) => {
 function countdown(x) {
  if (x> 0) {
    notify(`${20 * x}% remaining`);
    setTimeout(() => countdown(x - 1), 1000);
  } else {
   resolve();
  }
 }

 countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, 'progress:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (after 1s) 80% remaining
// (after 2s) 60% remaining
// (after 3s) 40% remaining
// (after 4s) 20% remaining
// (after 5s) completed 

This notify() method is designed to be chainable by returning itself, and handler execution will be preserved on a per-notification basis, as shown here:



p.notify((x) => setTimeout(console.log, 0, 'a:', x))
.notify((x) => setTimeout(console.log, 0, 'b:', x));

p.then(() => setTimeout(console.log, 0, 'completed'));

// (after 1s) a: 80% remaining
// (after 1s) b: 80% remaining
// (after 2s) a: 60% remaining
// (after 2s) b: 60% remaining
// (after 3s) a: 40% remaining
// (after 3s) b: 40% remaining
// (after 4s) a: 20% remaining
// (after 4s) b: 20% remaining
// (after 5s) completed 

Overall, this is a relatively crude implementation, but it should demonstrate how such a notification feature could be useful.

ASYNC FUNCTIONS

Async functions, also referred to by the operative keyword pair “async/await,” are the application of the ES6 Promise paradigm to ECMAScript functions. Support for async/await was introduced in the ES7 specification. It is both a behavioral and syntactical enhancement to the specification that allows for JavaScript code which is written in a synchronous fashion, but actually is able to behave asynchronously. The simplest example of this begins with a simple promise, which resolves with a value after a timeout:

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

This promise will resolve with a value of 3 after 1000ms. For other parts of the program to access this value once it is ready, it will need to exist inside a resolved handler:

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

p.then((x) => console.log(x)); // 3

This is fairly inconvenient, as the rest of the program now needs to be shoehorned into the promise handler. It is possible to move the handler into a function definition:

function handler(x) { console.log(x); }

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

p.then(handler); // 3

This isn't much of an improvement. The fact remains that any subsequent code that wishes to access the value produced by the promise needs to be fed that value through a handler, which means being shoved into a handler function. The ES7 offers async/await as an elegant solution to this problem.

Async Function Basics

ES7's async/await is intended to directly address the issue of organizing code which makes use of asynchronous constructs. It introduces a logical extension of asynchronous behavior into the domain of JavaScript functions by introducing two new keywords, async and await.

The async keyword

An async function can be declared by prepending the async keyword. This keyword can be used on function declarations, function expressions, arrow functions, and methods:

async function foo() {}

let bar = async function() {};

let baz = async () => {};

class Qux {
 async qux() {}
}

Using the async keyword will create a function that exhibits some asynchronous characteristics but overall is still synchronously evaluated. In all other respects such as arguments and closures, it still exhibits all the normal behavior of a JavaScript function. Consider this simple demonstration here, which shows the foo() function is still evaluated before proceeding to subsequent instructions:

async function foo() {
 console.log(1);
}

foo();
console.log(2);

// 1
// 2

In an async function, whatever value is returned with the return keyword (or undefined if there is no return) will be effectively converted into a promise object with Promise.resolve(). An async function will always return a promise object. Outside the function, the evaluated function will be this promise object:

async function foo() {
 console.log(1);
 return 3;
}

// Attach a resolved handler to the returned promise
foo().then(console.log);

console.log(2);

// 1
// 2
// 3

Of course, this means returning a promise object affords identical behavior:

async function foo() {
 console.log(1);
 return Promise.resolve(3);
}

// Attach a resolved handler to the returned promise
foo().then(console.log);

console.log(2);

// 1
// 2
// 3

The return value of an async function anticipates, but does not actually require, a thenable object: It will also work with regular values. A thenable object will be “unwrapped” via the first argument provided to the then() callback. A non-thenable object will be passed through as if it were an already resolved promise. These various scenarios are demonstrated here:

// Return a primitive
async function foo() {
 return 'foo';
}
foo().then(console.log);
// foo

// Return a non-thenable object
async function bar() {
 return ['bar'];
}
bar().then(console.log);
// ['bar']

// Return a thenable non-promise object
async function baz() {
 const thenable = {
  then(callback) { callback('baz'); }
 };
 return thenable;
}
baz().then(console.log);
// baz

// Return a promise
async function qux() {
 return Promise.resolve('qux');
}
qux().then(console.log);
// qux

As with promise handler functions, throwing an error value will instead return a rejected promise:

async function foo() {
 console.log(1);
 throw 3;
}

// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);

// 1
// 2
// 3

However, promise rejection errors will not be captured by the async function:

async function foo() {
 console.log(1);
 Promise.reject(3);
}

// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);

// 1
// 2
// Uncaught (in promise): 3

The await keyword

Because an async function indicates to code invoking it that there is no expectation of timely completion, the logical extension of this behavior is the ability to pause and resume execution. This exact feature is possible using the await keyword, which is used to pause execution while waiting for a promise to resolve. Consider the example from the beginning of the chapter, shown here:

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

p.then((x) => console.log(x)); // 3

This can be rewritten using async/await as follows:

async function foo() {
 let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
 console.log(await p);
}

foo();
// 3

The await keyword will pause execution of the async function, releasing the JavaScript runtime's thread of execution. This behavior is not unlike that of the yield keyword in a generator function. The await keyword will attempt to “unwrap” the object's value, pass the value through to the expression, and asynchronously resume execution of the async function.

The await keyword is used in the same way as a JavaScript unary operator. It can be used standalone or inside an expression, as shown in the following examples:

// Asynchronously print "foo"
async function foo() {
 console.log(await Promise.resolve('foo'));
}
foo();
// foo


// Asynchronously print "bar"
async function bar() {
 return await Promise.resolve('bar');
}
bar().then(console.log);
// bar

// Asynchronously print "baz" after 1000ms
async function baz() {
 await new Promise((resolve, reject) => setTimeout(resolve, 1000));
 console.log('baz');
}
baz();
// baz <after 1000ms>

The await keyword anticipates, but does not actually require, a thenable object: It will also work with regular values. A thenable object will be “unwrapped” via the first argument provided to the then() callback. A non-thenable object will be passed through as if it were an already resolved promise. These various scenarios are demonstrated here:

// Await a primitive
async function foo() {
 console.log(await 'foo');
}
foo();
// foo

// Await a non-thenable object
async function bar() {
 console.log(await ['bar']);
}
bar();
// ['bar']

// Await a thenable non-promise object
async function baz() {
 const thenable = {
  then(callback) { callback('baz'); }
 };
 console.log(await thenable);
}
baz();
// baz

// Await a promise
async function qux() {
 console.log(await Promise.resolve('qux'));
}
qux();
// qux

Awaiting a synchronous operation that throws an error will instead return a rejected promise:

async function foo() {
 console.log(1);
 await (() => { throw 3; })();
}

// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);

// 1
// 2
// 3

As shown earlier, a standalone Promise.reject() will not be captured by an async function and will throw as an unhandled error. However, using await on a promise that rejects will unwrap the error value:

async function foo() {
 console.log(1);
 await Promise.reject(3);
 console.log(4); // never prints
}

// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);

// 1
// 2
// 3 

Restrictions on await

The await keyword must be used inside an async function; it cannot be used in a top-level context such as a script tag or module. However, there is nothing preventing you from immediately invoking an async function. The following two code snippets are effectively identical:

async function foo() {
 console.log(await Promise.resolve(3));
}
foo();
// 3

// Immediately invoked async function expression
(async function() {
 console.log(await Promise.resolve(3));
})();
// 3 

Furthermore, the async nature of a function does not extend to nested functions. Therefore, the await keyword can also only appear directly inside an async function definition; attempting to use await inside a synchronous function will throw a SyntaxError.

Shown below are a few disallowed examples:

// Not allowed: 'await' inside arrow function
function foo() {
 const syncFn = () => {
  return await Promise.resolve('foo');
 };
 console.log(syncFn());
}

// Not allowed: 'await' inside function declaration
function bar() {
 function syncFn() {
  return await Promise.resolve('bar');
 }
 console.log(syncFn());

// Not allowed: 'await' inside function expression
function baz() {
 const syncFn = function() {
 return await Promise.resolve('baz');
 };
 console.log(syncFn());
}

// Not allowed: IIFE using function expression or arrow function
function qux() {
 (function () { console.log(await Promise.resolve('qux')); })();
 (() => console.log(await Promise.resolve('qux')))();
}

Halting and Resuming Execution

The true nature of using the await keyword is more nuanced than it might initially appear. For example, consider the following example where three functions are invoked in order but their outputs print in reverse:

async function foo() {
 console.log(await Promise.resolve('foo'));
}

async function bar() {
 console.log(await 'bar');
}

async function baz() {
 console.log('baz');
}

foo();
bar();
baz();

// baz
// bar
// foo

In the paradigm of async/await, the await keyword is doing all the heavy lifting; the async keyword is, in many ways, just a special indicator to the JavaScript interpreter. After all, an async function that doesn't contain the await keyword executes much like a regular function:

async function foo() {
 console.log(2);
}

console.log(1);
foo();
console.log(3);

// 1
// 2
// 3 

The key to fully understanding the await keyword is that it doesn't just wait for a value to become available. When the await keyword is encountered, the JavaScript runtime can track exactly where the execution was paused. When the value to the right of await is ready, the JavaScript runtime will push a task on the message queue that will asynchronously resume execution of that function.

Therefore, even when await is paired with an immediately available value, the rest of the function will still be asynchronously evaluated. This is demonstrated in the following example:

async function foo() {
 console.log(2);
 await null;
 console.log(4);
}

console.log(1);
foo();
console.log(3);

// 1
// 2
// 3
// 4 

The order of the console readout is best explained in terms of how the runtime handles this example:

  1. Print 1.
  2. Invoke async function foo.
  3. (inside foo) Print 2.
  4. (inside foo) await keyword pauses execution and adds task on message queue for immediately available value of null.
  5. foo exits.
  6. Print 3.
  7. Synchronous thread of execution finishes.
  8. JavaScript runtime dequeues task off message queue to resume execution.
  9. (inside foo) Execution resumes; await is provided with the value null (which, here, goes unused).
  10. (inside foo) Print 4.
  11. foo returns.

Using await with a promise makes this scenario more complicated. In this case, there will, in fact, be two separate message queue tasks that are evaluated asynchronously to complete execution of the async function. The following example, which might seem completely counterintuitive, demonstrates the order of execution:

async function foo() {
 console.log(2);
 console.log(await Promise.resolve(8));
 console.log(9);
}

async function bar() {
 console.log(4);
 console.log(await 6);
 console.log(7);
}

console.log(1);
foo();
console.log(3);
bar();
console.log(5);

// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9 

The runtime will execute this example as follows:

  1. Print 1.
  2. Invoke async function foo.
  3. (inside foo) Print 2.
  4. (inside foo) await keyword pauses execution and schedules task to be added to message queue when the promise settles.
  5. Promise is immediately resolved; task to provide resolved value to await is added to message queue.
  6. foo exits.
  7. Print 3.
  8. Invoke async function bar.
  9. (inside bar) Print 4.
  10. (inside bar) await keyword pauses execution and adds task on message queue for immediately available value of 6.
  11. bar exits.
  12. Print 5.
  13. Top-level thread of execution finishes.
  14. JavaScript runtime dequeues resolved await promise handler and provides it with the resolved value of 8.
  15. JavaScript runtime enqueues task to resume execution of foo onto message queue.
  16. JavaScript runtime dequeues task to resume execution of bar with value 6 off message queue.
  17. (inside bar) Execution resumes, await is provided with the value 6.
  18. (inside bar) Print 6.
  19. (inside bar) Print 7.
  20. bar returns.
  21. Asynchronous task completes, JavaScript dequeues task to resume execution of foo with value 8.
  22. (inside foo) Print 8.
  23. (inside foo) Print 9.
  24. foo returns.

Strategies for Async Functions

Because of their convenience and utility, async functions are increasingly a ubiquitous pillar of JavaScript codebases. Even so, when wielding async functions, keep certain considerations in mind.

Implementing Sleep()

When learning JavaScript for the first time, many developers will reach for a construct similar to Java's Thread.sleep() in an attempt to introduce a non-blocking delay into their program. Formerly, this was a cornerstone pedagogical moment for introducing how setTimeout fit into the JavaScript runtime's behavior.

With async functions, this is no longer the case! It is trivial to build a utility that allows a function to sleep() for a variable number of milliseconds:

async function sleep(delay) {
 return new Promise((resolve) => setTimeout(resolve, delay));
}

async function foo() {
 const t0 = Date.now();
 await sleep(1500); // sleep for ~1500ms
 console.log(Date.now() - t0);
}
foo();
// 1502

Maximizing Parallelization

If the await keyword is not used carefully, your program may be missing out on possible parallelization speedups. Consider the following example, which awaits five random timeouts sequentially:

async function randomDelay(id) {
 // Delay between 0 and 1000 ms
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
  console.log(`${id} finished`);
  resolve();
 }, delay));
}

async function foo() {
 const t0 = Date.now();
 await randomDelay(0);
 await randomDelay(1);
 await randomDelay(2);
 await randomDelay(3);
 await randomDelay(4);
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();

// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 2219ms elapsed 

Rolling up into a for loop yields the following:

async function randomDelay(id) {
 // Delay between 0 and 1000 ms
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
  console.log(`${id} finished`);
  resolve();
 }, delay));
}

async function foo() {
 const t0 = Date.now();
 for (let i = 0; i < 5; ++i) {
  await randomDelay(i);
 }
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();

// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 2219ms elapsed 

Even though there is no interdependence between the promises, this async function will pause and wait for each to complete before beginning the next. Doing so guarantees in-order execution, but at the expense of total execution time.

If in-order execution is not required, it is better to initialize the promises all at once and await the results as they become available. This can be done as follows:

async function randomDelay(id) {
 // Delay between 0 and 1000 ms
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
  setTimeout(console.log, 0, `${id} finished`);
  resolve();
 }, delay));
}

async function foo() {
 const t0 = Date.now();

 const p0 = randomDelay(0); 
 const p1 = randomDelay(1);
 const p2 = randomDelay(2);
 const p3 = randomDelay(3);
 const p4 = randomDelay(4);

 await p0;
 await p1;
 await p2;
 await p3;
 await p4;

 setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();

// 1 finished
// 4 finished
// 3 finished
// 0 finished
// 2 finished
// 2219ms elapsed 

Rolling up into an array and for loop yields the following:

async function randomDelay(id) {
 // Delay between 0 and 1000 ms
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
  console.log(`${id} finished`);
  resolve();
 }, delay));
}

async function foo() {
 const t0 = Date.now();

 const promises = Array(5).fill(null).map((_, i) => randomDelay(i));

 for (const p of promises) {
  await p;
 }

 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();

// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// 877ms elapsed 

Note that, although the execution of the promises is out of order, the await statements are provided to the resolved values in order:

async function randomDelay(id) {
 // Delay between 0 and 1000 ms
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
  console.log(`${id} finished`);
  resolve(id);
 }, delay));
}

async function foo() {
 const t0 = Date.now();

 const promises = Array(5).fill(null).map((_, i) => randomDelay(i));

 for (const p of promises) {
  console.log(`awaited ${await p}`);
 }

 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();

// 1 finished
// 2 finished
// 4 finished
// 3 finished
// 0 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed 

Serial Promise Execution

In the “Promises” section of this chapter, there is a discussion on how to compose promises that serially execute and pass values to the subsequent promise. With async/await, chaining promises becomes trivial:

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}

async function addTen(x) {
 for (const fn of [addTwo, addThree, addFive]) {
  x = await fn(x);
 }
 return x;
}

addTen(9).then(console.log); // 19 

Here, await is directly passed the return value of each function and the result is iteratively derived. The preceding example does not actually deal in promises, but it can be reconfigured to use async functions—and therefore promises—instead:

async function addTwo(x) {return x + 2;}
async function addThree(x) {return x + 3;}
async function addFive(x) {return x + 5;}

async function addTen(x) {
 for (const fn of [addTwo, addThree, addFive]) {
  x = await fn(x);
 }
 return x;
}

addTen(9).then(console.log); // 19 

Stack Traces and Memory Management

Promises and async functions have a considerable degree of overlap in terms of functionality they provide, but they diverge considerably when it comes to how they are represented in memory. Consider the following example, which shows the stack trace readout for a rejected promise:

function fooPromiseExecutor(resolve, reject) {
 setTimeout(reject, 1000, 'bar');
}

function foo() {
 new Promise(fooPromiseExecutor);
}

foo();
// Uncaught (in promise) bar
//  setTimeout
//  setTimeout (async)
// fooPromiseExecutor
// foo

Based on your understanding of promises, this stack trace readout should puzzle you a bit. The stack trace should, quite literally, represent the nested nature of function calls that exists currently in the JavaScript engine's memory stack. When the timeout handler executes and rejects the promise, the error readout shown identifies the nested functions that were invoked to create the promise instance originally. However, it is known that these functions have already returned and therefore wouldn't be found in a stack trace.

The answer is simply that the JavaScript engine does extra work to preserve the call stack while it can when creating the promise structure. When an error is thrown, this call stack is retrievable by the runtime's error handling logic and therefore is available in the stack trace. This, of course, means it must preserve the stack trace in memory, which comes at a computational and storage cost.

Consider the previous example if it were reworked with async functions, as demonstrated here:

function fooPromiseExecutor(resolve, reject) {
 setTimeout(reject, 1000, 'bar');
}

async function foo() {
 await new Promise(fooPromiseExecutor);
}
foo();

// Uncaught (in promise) bar
// foo
// async function (async)
// foo

With this structure, the stack trace is accurately representing the current call stack because fooPromiseExecutor has returned and is no longer on the stack, but foo is suspended and has not yet exited. The JavaScript runtime can merely store a pointer from a nested function to its container function, as it would with a synchronous function call stack. This pointer is efficiently stored in memory and can be used to generate the stack trace in the event of an error. Such a strategy does not incur extra overhead as is the case with the previous example, and therefore should be preferred if performance is critical to your application.

SUMMARY

Mastering asynchronous behavior inside the single-threaded JavaScript runtime has long been an imposing task. With the introduction of promises in ES6 and async/await ES7, asynchronous constructs in ECMAScript have been greatly enhanced. Not only do promises and async/await enable patterns that were previously difficult or impossible to implement, but they engender an entirely new way of writing JavaScript that is cleaner, shorter, and easier to understand and debug.

Promises are built to offer a clean abstraction around asynchronous code. They can represent an asynchronously executed block of code, but they can also represent an asynchronously calculated value. They are especially useful in a situation where the need to serialize asynchronous code blocks arises. Promises are a delightfully malleable construct: They can be serialized, chained, composed, extended, and recombined.

Async functions are the result of applying the promise paradigm to JavaScript functions. They introduce the ability to suspend execution of a function without blocking the main thread of execution. They are tremendously useful both in writing readable promise-centric code as well as in managing serialization and parallelization of asynchronous code. They are one of the most important tools in the modern JavaScript toolkit.

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

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