Chapter 3: Synchronizing with Promises

Implementations of promises have existed for many years inside JavaScript libraries. It all started with the Promises/A+ specification. Libraries implemented their own variations of this specification, and it wasn't until recently (ES6 to be exact) that the Promise specification made it into the JavaScript language. They do what the chapter title suggests—help us apply the synchronization principle.

In this chapter, we'll start of with a gentle introduction to the various terms used in promise-speak so that the remainder of the chapter will be a little easier to follow. Then, well go through the various ways that promises are used to resolve future values and make our lives easier when we deal with concurrency. Ready?

Promise terminology

Before we dive right into the code examples, let's take a minute to make sure we have a firm grasp on the terminology surrounding promises. There are promise instances, but then there are also various states and actions to consider. The sections that follow will make much more sense if we can nail down the promise lexicon. These explanations are short and sweet, so if you've already used promises, you can quickly gloss over these definitions to sanity check your knowledge.

Promise

As the name suggests, a promise is, well, a promise. Think of a promise as a proxy for a value that doesn't exist yet. The promise let's us write better concurrent code because we know that the value will be there at some point, and we don't have to write lots of state-checking boilerplate code.

State

Promises are always in one of three states:

  • Pending: This is the first state of a promise after it's been created. It remains in a pending state until it's fulfilled or rejected.
  • Fulfilled: The promise value has been resolved and is available to the then() callback function.
  • Rejected: Something went wrong trying to resolve the promised value. There will be no data today.

An interesting property of promise states is that they only transition once. They either go from pending to fulfilled or from pending to rejected. And once they make this state transition, they're stuck in this state for the rest of their existence.

Executor

The executor function is responsible for somehow resolving the value that the caller is waiting for. This function is called immediately after the promise is created. It takes two arguments: a resolver function and a rejector function.

Resolver

The resolver is a function that's passed to the executor function as an argument. Actually, this is quite handy because we can then pass the resolver function to another function, and so on. It doesn't matter where the resolver function is called from, but when it's called, the promise moves into a fulfilled state. This change in state will trigger any then() callbacks—we'll see what these are shortly.

Rejector

It's the second argument passed to the executor function, which can be called from anywhere. When it's called, it changes the state of the promise from pending to rejected. This state change will call the error callback function, if any, passed to then() or catch().

Thenable

An object is thenable if it has a then() method that accepts a fulfillment callback and a rejection callback as arguments. In other words, a promise is thenable. But there are cases where we might want to implement specialized resolution semantics.

Resolving and rejecting promises

If the preceding section just introduced several new terms that sounded confusing, then don't worry. We'll see what all these promise terms look like in practice, starting with this section. Here, we'll perform some straightforward promise resolving and rejecting.

Resolving promises

The resolver is a function that, as the name implies, resolves a promise for us. It's not the only way to resolve a promise—we'll explore more advanced techniques later on in the chapter. But this method is, by far, the most common. It's passed into the executor function as the first argument. This means that the executor can resolve the promise directly by simply calling the resolver. But this wouldn't provide us with much utility, would it?

The common case to a greater extent is for the promise executor function to set up the asynchronous actions that are about to take place—things such as making network calls. Then, in the callback functions for these asynchronous actions, we can resolve the promise. It's a little counterintuitive at first, passing a resolve function around in our code, but it'll make more sense once we start using them.

A resolver function is an opaque function that's bound to a promise. It can only resolve a promise once. We can call the resolver as many times as we please, but only the first call will change the state of the promise. Now, let's take a look at some promise code. Here, we'll resolve a promise, which causes the then() fulfillment callback function to be called:

As we can see, the fulfilled() function is called when the resolver function is called. The executor doesn't actually call the resolver. Rather, it passes the resolver function to another asynchronous function—setTimeout(). The executor function itself isn't the asynchronous code that we're trying to wrangle. The executor can be thought of as a sort of coordinator, orchestrating asynchronous actions to determine when to resolve the promise.

The preceding example didn't resolve any values. This is a valid use cases when the caller of some action needs acknowledgement that it either succeeded or failed. Instead, let's try resolving a value this time, as follows:

We can see that this code is very similar to the preceding example. The difference is that our resolver function is actually called within the closure of the callback function that's passed to setTimeout(). This is because we're resolving a string value. There's also an argument that's passed to our fulfilled() function, which is the resolved value.

Rejecting promises

The promise executor function doesn't always go as planned, and when this happens, we need to reject the promise. This is the other possible state transition from pending. Instead of moving into a fulfilled state, the promise moves into a rejected state. This causes a different callback to execute, separate from the fulfillment callback. Thankfully, the mechanics of rejecting promises is very similar to resolving them. Let's take a look at how this is done:

This code looks very familiar to the resolution code that we looked at in the preceding section. We set a timeout, and instead of resolving the function, we rejected it. This is done using the rejector function and is passed into the executor as the second argument.

We use the catch() method instead of the then() method to setup our rejection callback function. The rejection callback in this example simply logs the reason for the failure as an error. It's always important to provide this value. When we resolve promises, a value is common, although not strictly necessary. With rejections, on the other hand, there isn't a viable case for not providing the reason for the rejection even if the callback is only logging the error.

Let's look at another example, one that catches exceptions in the executor, and provides the rejected callbacks with a more meaningful interpretation of the failure:

What's interesting about the first promise in the previous example is that it does change state, even though we're not explicitly changing the state of the promise using resolve() or reject(). However, it's important for the promise to eventually change state; we'll explore this topic in the next section.

Empty promises

Despite the fact that the executor function passes a resolver function and a rejector function, there's never any guarantee that the promise will change state. In this scenario, the promise simply hangs, and neither the resolved callback nor the rejected callback is triggered. This may not seem like a problem, and in fact, with simple promises, it's easy to diagnose and fix these unresponsive promises. However, as we get into more elaborate scenarios later in the chapter, a promise can be resolved as a result of several other promises resolving. If one of these promises doesn't resolve or reject, then the whole flow falls apart. This scenario is very time-consuming to debug. Let's now look at an executor function that causes a promise to hang:

But what if there was a safer way to deal with this uncertainty? An executor function with the potential to hang indefinitely without resolving or rejecting is hardly something we want in our code. Let's look at implementing an executor wrapper function that acts as a safety net by rejecting promises that take too long to resolve. This would take the mystery out of diagnosing complex promise scenarios:

Reacting to promises

Now that we have a better understanding of the mechanics of executing promises, this section will take a closer look at using promises to solve particular problems. Typically, this means reacting with some purpose in mind when the promise is fulfilled or rejected.

We'll start off by looking at the job queues inside the JavaScript interpreter, and what these mean for our resolution callback functions. We'll then look at making use of the promised data, dealing with errors, creating better abstractions for reacting to promises, and thenables. Let's get going.

Resolution job queues

The concept of the JavaScript job queue was introduced in Chapter 2, The JavaScript Execution Model. Its main responsibility is to initiate new execution context stacks. This is the main job queue. However, there's another queue, which is dedicated to the callbacks executed by promises. This means that the algorithm responsible for selecting the next job to run can select from either of the queues if they're both populated.

Promises have concurrency semantics built into them, and with good reason. If a promise is used to ensure that a value is eventually resolved, it makes sense to give high priority to the code that reacts to it. Otherwise, when the value arrives, the code that processes it might have to wait in a longer line behind other jobs.

The same semantics are followed with rejected callbacks too.

Let's write some code that demonstrates these concurrency semantics:

Using promised data

So far, we've seen a few examples in this chapter where a resolver function resolves a promise with a value. The value that's passed to this function is the value that's ultimately passed to the fulfilled callback function. The idea is for the executor to set up any asynchronous action, such as setTimeout(), which would later call the resolver with the value. But in these examples, the caller isn't actually waiting on any values; we merely use setTimeout() as an example asynchronous action. Let's look at a case where we don't actually have a value, and an asynchronous network request needs to go get it:

With functions like get(), not only do they consistently return a synchronization primitive like a promise, but they also encapsulate some nasty asynchronous details. Dealing with XMLHttpRequest objects all over the place in our code isn't pleasant. We've also simplified various modes with which the response may come back. Instead of always having to create handlers for the load, error, and abort events, we only have one interface to worry about—the promise. This is what the synchronize concurrency principle is all about.

Error callbacks

There are two ways to react to rejected promises. Put differently, supplying error callbacks. The first approach is to use the catch() method, which takes a single callback function. The alternative is to pass in the rejected callback function as the second argument to then().

The then() approach that is used to supply rejected callback functions is superior in a couple of scenarios, and it should probably be used instead of catch(). The first scenario is writing our code so that promises and thenable objects are interchangeable. The catch() method isn't necessarily part of a thenable. The second scenario is when we build callback chains, which we will explore later on in this chapter. Let's look at some code that compares the two approaches for providing rejected callback functions to promises:

We can see here that both approaches are actually very similar. There's no real advantage to one over the other in terms of code aesthetics. However, there's an advantage to the then() approach when it comes to using thenables, which we'll see shortly. But, since we're not actually using the promise instance in any way, other than to add the callbacks, there's really no need to worry about catch() versus then() for registering error callbacks.

Always reacting

Promises always end up in either a fulfilled state or a rejected state. We generally have distinct callback functions for each of these states. However, there's a strong possibility that we'll want to perform some of the same actions for both states. For example, if a component that uses a promise changes state while the promise is pending, we'll want to make sure that the state is cleaned up once the promise is resolved or rejected.

We could write our code in such a way that the callbacks for fulfilled and rejected states each perform these actions themselves, or that they can each call some common function that does the cleanup.

Wouldn't it make more sense to assign the cleanup responsibility to the promise, instead of assigning it to the individual outcomes? This way, the callback function that runs when the promise is resolved is focused on what it needs to do with the value, and the rejection callback is focused on dealing with the error. Let's see if we can write some code that extends promises with an always() method:

Note that the order is important here. If we called always() before then(), then the function would still always run, but it would run before the callbacks provided to then(). We could actually call always() before and after then() to always run code before the fulfilled or rejected callbacks, and after.

Resolving other promises

Most of the promises that we've seen so far in this chapter have either been resolved directly by the executor function or as the result of calling the resolver from an asynchronous action, when the value was ready to resolve. Passing the resolver function around like this is actually quite flexible. For example, the executor doesn't even have to perform any work except for storing the resolver function somewhere for it to be called later on to resolve the promise.

This can be especially useful when we find ourselves in more complex synchronization scenarios that require multiple values, which have been promised to callers. If we have the resolver function, we can resolve the promise. Let's take a look at code that stores the resolver function of several promises so that each promise can be resolved later on:

As this example makes clear, we don't have to resolve anything within the executor function itself. In fact, we don't even need to explicitly reference promise instances after they've been created and set up with executors and fulfillment functions. The resolver function has been stored somewhere, and it holds a reference to the promise.

Promise–like objects

The Promise class is a primitive JavaScript type. However, we don't always need to create new promise instances to implement the same behavior for synchronizing actions. There's a static Promise.resolve() method that we can use to resolve such objects. Let's see how this method is used:

Building callback chains

Each promise method that we examined so far in this chapter returns promises. This allows us to call these methods again on the return value, resulting in a chain of then().then() calls, and so forth. One challenging aspect of chaining promise calls together is that the instances returned by promise methods are new instances. That is, there's a degree of immutability to the promises that we'll explore in this section.

As our application gets larger, the concurrency challenges grow with it. This means that we need to think of better ways to leverage synchronization primitives, such as promises. Just as any other primitive value in JavaScript, we can pass them around from function to function. We have to treat promises in the same way—passing them around, and building upon the chain of callback functions.

Promises only change state once

Promises are born into a pending state, and they die in either a resolved or rejected state. Once a promise has transitioned into one of these states, they're stuck in this state. This has two interesting side-effects.

First, multiple attempts to resolve or reject a promise are ignored. In other words, resolvers and rejectors are idempotent—only the first call has any effect on the promise. Let's see how this looks code-wise:

The other implication of promises changing state only once is that the promise could actually resolve before a fulfillment or rejection callback is added. Race conditions, such as this one, are the harsh reality of concurrent programming. Typically, the callback function is added to the promise at the time of creation. Since JavaScript is run-to-completion, the job queue that processes promise resolution callbacks isn't serviced until the callback is added. But, what if the promise resolves immediately in the executor? What if the callback is added to the promise in another JavaScript execution context? Let's see if we can better illustrate these ideas with some code:

This code illustrates a very important property of promises. It doesn't matter when our fulfillment callbacks are added to the promise, whether it's in a pending state, or a fulfilled state, the code that uses the promise doesn't change. On the face of it, this may not seem like a big deal. But this type of race condition checking would require more concurrency code for us to maintain ourselves. Instead, the Promise primitive handles this for us, and we can start treating asynchronous values as primitive types.

Immutable promises

Promises aren't truly immutable. They change state, and the then() method adds callback functions to the promise. However, there are some immutable traits of promises that are worth discussing here, as they impact our promise code in certain situations.

Technically speaking, the then() method doesn't actually mutate the promise object. It creates what's called a promise capability, which is an internal JavaScript record that references the promise, and the functions that we add. So, it's not a real reference in the JavaScript sense of the term.

The then() method does not return the same instance it was called with as the context. Instead, then() creates a new promise instance and returns that. Let's take a look at some code to examine more closely what happens when we chain together promises using then():

We can clearly see that the two promise instances created in this example are separate promise objects. Something else that's worth pointing out is that the second promise is bound to the first one—it resolves when the first promise resolves. However, we can see that the value isn't passed to the second promise. We'll address this problem in the following section.

Many then callbacks, many promises

As we saw in the preceding section, promises created with then() are bound to their creator. That is, when the first promise is resolved, the promise that's bound it it also resolves, and so on. However, we noticed a slight problem as well. The resolved value doesn't make it past the first callback function. The reason our first callback gets the value as an argument is because this happens transparently within the promise mechanism. Let's take a look at another promise chain example. This time, we'll explicitly return the values from our callback functions:

This looks promising. Now we can see that the resolved value makes its way through the promise chain. There's a catch—the rejection isn't cumulative. Instead, only the first promise in the chain is actually rejected. The remaining promises are simply resolved, not rejected. This means that the last catch() callback will never run.

When we chain together promises in this fashion, our fulfillment callback functions need to be able to handle error conditions. For example, the value that's resolved could have an error property, which could be checked for specifics.

Passing promises around

In this section, we'll extend the idea of treating promises as primitive values. Something we often do with primitive values is pass them to functions as arguments, and return them from functions. The key difference between a promise and other primitives is how we use them. Other values exist now, whereas promised values will exist eventually. Therefore, we need to define some course of action via a callback function to take place when the value does arrive.

What's nice about promises is that the interface used to supply these callback functions is small and consistent. We don't need to invent synchronization mechanisms on the fly when we can couple the value with the code that will act upon it. These units can move around our application just like any other value, and the concurrency semantics are unobtrusive.

By the end of this function call stack, we have a promise object that's reflective of several promises resolving. The whole resolution chain is kicked off by the first promise resolving. What's more important than the mechanics of how the value traverses the chain of promises is the idea that all of these functions are free to use this promised value without affecting other functions.

There are two concurrency principles at play here. First, we will conserve by performing an asynchronous action to get the value only once; each of the callback functions are free to use this resolved value. Second, we're doing a good job of abstracting our synchronization mechanisms. In other words, the code doesn't feel like it's burdened with boilerplate concurrency code. Let's see what code that passes promises around actually looks like:

The key functions here are our update functions—updateFirstName(), updateLastName(), and updateAge(). They're very flexible and accept a promise or value resolved by a promise. If any of these functions get a promise as an argument, they return a new promise by adding a then() callback function. Note that it's adding the same function. updateFirstName() will add updateFirstName() as the callback. When the callback fires, it'll be with the plain object that's used to update the UI this time. So the promise check fails, and we can proceed to update the UI.

The promise checking takes all of three lines per function, which is not exactly obtrusive. The end result is the flexible code that's easy to read. Ordering doesn't matter; we could have composed our update() function in a different order, and the UI components would all be updated in the same way. We can pass the plain object directly to update() and everything will work the same. Concurrent code that doesn't look like concurrent code is our big win here.

Synchronizing several promises

Until this point in the chapter, we've looked at single promise instances that resolve a value, trigger callbacks, and possibly cause other promises to resolve. In this section, we'll look at a couple of static Promise methods that help us in scenarios where we need to synchronize the resolution of several promise values.

First, we'll address the common case where a component that we develop requires synchronous access to several asynchronous resources. Then, we'll look at the less common scenario where asynchronous actions become irrelevant before they're resolved due to events that have taken place in the UI.

Waiting on promises

In the case where we are waiting for several promises to resolve, perhaps to transform multiple data sources into something consumable by a UI component, we can use the Promise.all() method. It takes a collection of promise instances as input, and returns a new promise instance. This new instance is resolved only when all of the input promises are resolved.

The then() callback that we provide to the new promise, created by Promise.all(), is given an array of resolved values as input. These values correspond to the input promises in terms of index position. This is a very powerful synchronization mechanism, one that helps us fulfill the synchronize concurrency principle because it hides all the bookkeeping.

Instead of several callbacks that each need to coordinate the state of the promises that they're bound to, we have one callback, which has all the resolved data that we need. Here's an example that shows how to synchronize multiple promises:

Cancelling promises

The XHR requests that we've seen so far in this book have handlers for aborted requests. This is because we can manually abort the request and prevent any load callbacks from running. A typical scenario that requires this functionality is for the user to click a cancel button, or navigate to a different part of the application, rendering the request redundant.

If we were to move up a level on the abstraction ladder to promises, the same principle applies. Something could happen while the concurrent action is executing that renders the promise pointless. The difference between promises and XHR requests, of course, is that the former has no abort() method. The last thing we want to do is start introducing unnecessary cancellation logic in our promise callbacks.

This is where the Promise.race() method can help us. As the name suggests, the method returns a new promise that's resolved by the first of the input promises to resolve. This may not sound like much, but implementing the logic of Promise.race() isn't easy. It's the synchronize principle in action, hiding concurrency complexities from the application code. Let's take a look at how this method can help us deal with cancelled promises due to user interactions:

As an exercise, try to imagine a more complex scenario where dataPromise is a promise created by Promise.all(). Our cancelResolver() function would be able to seamlessly cancel many complex asynchronous actions at once.

Promises without executors

In this final section, we'll look at the Promise.resolve() and Promise.reject() methods. We've already seen how Promise.resolve() can resolve thenable objects earlier in the chapter. It can also directly resolve values or other promises. These methods come in handy when we implement a function that has the potential to be both synchronous and asynchronous. This isn't a situation we want to find ourselves in, using a function with ambiguous concurrency semantics. For example, here's a function that's both, synchronous and asynchronous, leading to confusion, and almost certainly to bugs later on:

We can see that the last call returns a cached value, instead of a promise. This makes intuitive sense because we're not promising an eventual value, we already have it! The problem is that we're exposing an inconsistency to any code that uses our getData() function. That is, the code that calls getData() needs to handle concurrency semantics. This code is not concurrent. Let's change this by introducing Promise.resolve():

This is better. Using Promise.resolve() and Promise.reject(), any code that uses getData() will get concurrency by default, even when the data fetching action is synchronous.