Chain Flow
We’ve hinted at this a couple of times already, but Promises are not just a mechanism for a single-step this-then-that sort of operation. That’s the building block, of course, but it turns out we can string multiple Promises together to represent a sequence of async steps.
The key to making this work is built on two behaviors intrinsic to Promises:
- Every time you call
then(..)
on a Promise, it creates and returns a new Promise, which we can chain with. - Whatever value you return from the
then(..)
call’s fulfillment callback (the first parameter) is automatically set as the fulfillment of the chained Promise (from the first point).
Let’s first illustrate what that means, and then we’ll derive how that helps us create async sequences of flow control. Consider the following:
var p = Promise.resolve( 21 );
var p2 = p.then( function(v){
console.log( v ); // 21
// fulfill `p2` with value `42`
return v * 2;
} );
// chain off `p2`
p2.then( function(v){
console.log( v ); // 42
} );
By returning v * 2
(i.e., 42
), we fulfill the p2
promise that the first then(..)
call created and returned. When p2
‘s then(..)
call runs, it’s receiving the fulfillment from the return v * 2
statement. Of course, p2.then(..)
creates yet another promise, which we could have stored in a p3
variable.
But it’s a little annoying to have to create an intermediate variable p2
(or p3
, etc.). Thankfully, we can easily just chain these together:
var p = Promise.resolve( 21 );
p
.then( function(v){
console.log( v ); // 21
// fulfill the chained promise with value `42`
return v * 2;
} )
// here's the chained promise
.then( function(v){
console.log( v ); // 42
} );
So now the first then(..)
is the first step in an async sequence, and the second then(..)
is the second step. This could keep going for as long as you needed it to extend. Just keep chaining off a previous then(..)
with each automatically created Promise.
But there’s something missing here. What if we want step 2 to wait for step 1 to do something asynchronous? We’re using an immediate return
statement, which immediately fulfills the chained promise.
The key to making a Promise sequence truly async capable at every step is to recall how Promise.resolve(..)
operates when what you pass to it is a Promise or thenable instead of a final value. Promise.resolve(..)
directly returns a received genuine Promise, or it unwraps the value of a received thenable — and keeps going recursively while it keeps unwrapping thenables.
The same sort of unwrapping happens if you return
a thenable or Promise from the fulfillment (or rejection) handler. Consider:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// create a promise and return it
return new Promise( function(resolve,reject){
// fulfill with value `42`
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
Even though we wrapped 42
up in a promise that we returned, it still got unwrapped and ended up as the resolution of the chained promise, such that the second then(..)
still received 42
. If we introduce asynchrony to that wrapping promise, everything still nicely works the same:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// create a promise to return
return new Promise( function(resolve,reject){
// introduce asynchrony!
setTimeout( function(){
// fulfill with value `42`
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// runs after the 100ms delay in the previous step
console.log( v ); // 42
} );
That’s incredibly powerful! Now we can construct a sequence of however many async steps we want, and each step can delay the next step (or not!), as necessary.
Of course, the value passing from step to step in these examples is optional. If you don’t return an explicit value, an implicit undefined
is assumed, and the promises still chain together the same way. Each Promise resolution is thus just a signal to proceed to the next step.
To further the chain illustration, let’s generalize a delay-Promise creation (without resolution messages) into a utility we can reuse for multiple steps:
function delay(time) {
return new Promise( function(resolve,reject){
setTimeout( resolve, time );
} );
}
delay( 100 ) // step 1
.then( function STEP2(){
console.log( "step 2 (after 100ms)" );
return delay( 200 );
} )
.then( function STEP3(){
console.log( "step 3 (after another 200ms)" );
} )
.then( function STEP4(){
console.log( "step 4 (next Job)" );
return delay( 50 );
} )
.then( function STEP5(){
console.log( "step 5 (after another 50ms)" );
} )
...
Calling delay(200)
creates a promise that will fulfill in 200ms, and then we return that from the first then(..)
fulfillment callback, which causes the second then(..)
‘s promise to wait on that 200ms promise.
Note: As described, technically there are two promises in that interchange: the 200ms-delay promise and the chained promise that the second then(..)
chains from. But you may find it easier to mentally combine these two promises together, because the Promise mechanism automatically merges their states for you. In that respect, you could think of return delay(200)
as creating a promise that replaces the earlier-returned chained promise.
To be honest, though, sequences of delays with no message passing isn’t a terribly useful example of Promise flow control. Let’s look at a scenario that’s a little more practical.
Instead of timers, let’s consider making Ajax requests:
// assume an `ajax( {url}, {callback} )` utility
// Promise-aware ajax
function request(url) {
return new Promise( function(resolve,reject){
// the `ajax(..)` callback should be our
// promise's `resolve(..)` function
ajax( url, resolve );
} );
}
We first define a request(..)
utility that constructs a promise to represent the completion of the ajax(..)
call:
request( "http://some.url.1/" )
.then( function(response1){
return request( "http://some.url.2/?v=" + response1 );
} )
.then( function(response2){
console.log( response2 );
} );
Note: Developers commonly encounter situations in which they want to do Promise-aware async flow control with utilities that are not themselves Promise-enabled (like ajax(..)
here, which expects a callback). Although the native ES6 Promise
mechanism doesn’t automatically solve this pattern for us, practically all Promise libraries do. They usually call this process “lifting” or “promisifying” or some variation thereof. We’ll come back to this technique later.
Using the Promise-returning request(..)
, we create the first step in our chain implicitly by calling it with the first URL, and chain off that returned promise with the first then(..)
.
Once response1
comes back, we use that value to construct a second URL, and make a second request(..)
call. That second request(..)
promise is return
ed so that the third step in our async flow control waits for that Ajax call to complete. Finally, we print response2
once it returns.
The Promise chain we construct is not only a flow control that expresses a multistep async sequence, but it also acts as a message channel to propagate messages from step to step.
What if something went wrong in one of the steps of the Promise chain? An error/exception is on a per-Promise basis, which means it’s possible to catch such an error at any point in the chain, and that catching acts to sort of “reset” the chain back to normal operation at that point:
// step 1:
request( "http://some.url.1/" )
// step 2:
.then( function(response1){
foo.bar(); // undefined, error!
// never gets here
return request( "http://some.url.2/?v=" + response1 );
} )
// step 3:
.then(
function fulfilled(response2){
// never gets here
},
// rejection handler to catch the error
function rejected(err){
console.log( err ); // `TypeError` from `foo.bar()` error
return 42;
}
)
// step 4:
.then( function(msg){
console.log( msg ); // 42
} );
When the error occurs in step 2, the rejection handler in step 3 catches it. The return value (42
in this snippet), if any, from that rejection handler fulfills the promise for the next step (4), such that the chain is now back in a fulfillment state.
Note: As we discussed earlier, when returning a promise from a fulfillment handler, it’s unwrapped and can delay the next step. That’s also true for returning promises from rejection handlers, such that if the return 42
in step 3 instead returned a promise, that promise could delay step 4. A thrown exception inside either the fulfillment or rejection handler of a then(..)
call causes the next (chained) promise to be immediately rejected with that exception.
If you call then(..)
on a promise, and you only pass a fulfillment handler to it, an assumed rejection handler is substituted:
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// never gets here
}
// assumed rejection handler, if omitted or
// any other non-function value passed
// function(err) {
// throw err;
// }
);
As you can see, the assumed rejection handler simply rethrows the error, which ends up forcing p2
(the chained promise) to reject with the same error reason. In essence, this allows the error to continue propagating along a Promise chain until an explicitly defined rejection handler is encountered.
Note: We’ll cover more details of error handling with Promises a little later, because there are other nuanced details to be concerned about.
If a proper valid function is not passed as the fulfillment handler parameter to then(..)
, there’s also a default handler substituted:
var p = Promise.resolve( 42 );
p.then(
// assumed fulfillment handler, if omitted or
// any other non-function value passed
// function(v) {
// return v;
// }
null,
function rejected(err){
// never gets here
}
);
As you can see, the default fulfillment handler simply passes whatever value it receives along to the next step (Promise).
Note: The then(null,function(err){ .. })
pattern — only handling rejections (if any) but letting fulfillments pass through — has a shortcut in the API: catch(function(err){ .. })
. We’ll cover catch(..)
more fully in the next section.
Let’s review briefly the intrinsic behaviors of Promises that enable chaining flow control:
- A
then(..)
call against one Promise automatically produces a new Promise to return from the call. - Inside the fulfillment/rejection handlers, if you return a value or an exception is thrown, the new returned (chainable) Promise is resolved accordingly.
- If the fulfillment or rejection handler returns a Promise, it is unwrapped, so that whatever its resolution is will become the resolution of the chained Promise returned from the current
then(..)
.
While chaining flow control is helpful, it’s probably most accurate to think of it as a side benefit of how Promises compose (combine) together, rather than the main intent. As we’ve discussed in detail several times already, Promises normalize asynchrony and encapsulate time-dependent value state, and that is what lets us chain them together in this useful way.
Certainly, the sequential expressiveness of the chain (this-then-this-then-this…) is a big improvement over the tangled mess of callbacks as we identified in Chapter 2. But there’s still a fair amount of boilerplate (then(..)
and function(){ .. }
) to wade through. In the next chapter, we’ll see a significantly nicer pattern for sequential flow control expressivity, with generators.
Terminology: Resolve, Fulfill, and Reject
There’s some slight confusion around the terms “resolve,” “fulfill,” and “reject” that we need to clear up, before you get too much deeper into learning about Promises. Let’s first consider the Promise(..)
constructor:
var p = new Promise( function(X,Y){
// X() for fulfillment
// Y() for rejection
} );
As you can see, two callbacks (here labeled X
and Y
) are provided. The first is usually used to mark the Promise as fulfilled, and the second always marks the Promise as rejected. But what’s the “usually” about, and what does that imply about accurately naming those parameters?
Ultimately, it’s just your user code and the identifier names aren’t interpreted by the engine to mean anything, so it doesn’t technically matter; foo(..)
and bar(..)
are equally functional. But the words you use can affect not only how you are thinking about the code, but how other developers on your team will think about it. Thinking wrongly about carefully orchestrated async code is almost surely going to be worse than the spaghetti-callback alternatives.
So it actually does kind of matter what you call them.
The second parameter is easy to decide. Almost all literature uses reject(..)
as its name, and because that’s exactly (and only!) what it does, that’s a very good choice for the name. I’d strongly recommend you always use reject(..)
.
But there’s a little more ambiguity around the first parameter, which in Promise literature is often labeled resolve(..)
. That word is obviously related to “resolution,” which is what’s used across the literature (including this book) to describe setting a final value/state to a Promise. We’ve already used “resolve the Promise” several times to mean either fulfilling or rejecting the Promise.
But if this parameter seems to be used to specifically fulfill the Promise, why shouldn’t we call it fulfill(..)
instead of resolve(..)
to be more accurate? To answer that question, let’s also take a look at two of the Promise
API methods:
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Oops" );
Promise.resolve(..)
creates a Promise that’s resolved to the value given to it. In this example, 42
is a normal, non-Promise, non-thenable value, so the fulfilled promise fulfilledPr
is created for the value 42
. Promise.reject("Oops")
creates the rejected promise rejectedPr
for the reason "Oops"
.
Let’s now illustrate why the word “resolve” (such as in Promise.resolve(..)
) is unambiguous and indeed more accurate, if used explicitly in a context that could result in either fulfillment or rejection:
var rejectedTh = {
then: function(resolved,rejected) {
rejected( "Oops" );
}
};
var rejectedPr = Promise.resolve( rejectedTh );
As we discussed earlier in this chapter, Promise.resolve(..)
will return a received genuine Promise directly, or unwrap a received thenable. If that thenable unwrapping reveals a rejected state, the Promise returned from Promise.resolve(..)
is in fact in that same rejected state.
So Promise.resolve(..)
is a good, accurate name for the API method, because it can actually result in either fulfillment or rejection.
The first callback parameter of the Promise(..)
constructor will unwrap either a thenable (identically to Promise.resolve(..)
) or a genuine Promise:
var rejectedPr = new Promise( function(resolve,reject){
// resolve this promise with a rejected promise
resolve( Promise.reject( "Oops" ) );
} );
rejectedPr.then(
function fulfilled(){
// never gets here
},
function rejected(err){
console.log( err ); // "Oops"
}
);
It should be clear now that resolve(..)
is the appropriate name for the first callback parameter of the Promise(..)
constructor.
Warning: The previously mentioned reject(..)
does not do the unwrapping that resolve(..)
does. If you pass a Promise/thenable value to reject(..)
, that untouched value will be set as the rejection reason. A subsequent rejection handler would receive the actual Promise/thenable you passed to reject(..)
, not its underlying immediate value.
But now let’s turn our attention to the callbacks provided to then(..)
. What should they be called (both in literature and in code)? I would suggest fulfilled(..)
and rejected(..)
:
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
In the case of the first parameter to then(..)
, it’s unambiguously always the fulfillment case, so there’s no need for the duality of “resolve” terminology. As a side note, the ES6 specification uses onFulfilled(..)
and onRejected(..)
to label these two callbacks, so they are accurate terms.