Promise Patterns
We’ve already implicitly seen the sequence pattern with Promise chains (this-then-this-then-that flow control) but there are lots of variations on asynchronous patterns that we can build as abstractions on top of Promises. These patterns serve to simplify the expression of async flow control — which helps make our code more reason-able and more maintainable — even in the most complex parts of our programs.
Two such patterns are codified directly into the native ES6 Promise
implementation, so we get them for free, to use as building blocks for other patterns.
Promise.all([ .. ])
In an async sequence (Promise chain), only one async task is being coordinated at any given moment — step 2 strictly follows step 1, and step 3 strictly follows step 2. But what about doing two or more steps concurrently (aka “in parallel”)?
In classic programming terminology, a “gate” is a mechanism that waits on two or more parallel/concurrent tasks to complete before continuing. It doesn’t matter what order they finish in, just that all of them have to complete for the gate to open and let the flow control through.
In the Promise API, we call this pattern all([ .. ])
.
Say you wanted to make two Ajax requests at the same time, and wait for both to finish, regardless of their order, before making a third Ajax request. Consider:
// `request(..)` is a Promise-aware Ajax utility,
// like we defined earlier in the chapter
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// both `p1` and `p2` fulfill and pass in
// their messages here
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
Promise.all([ .. ])
expects a single argument, an array
, consisting generally of Promise instances. The promise returned from the Promise.all([ .. ])
call will receive a fulfillment message (msgs
in this snippet) that is an array
of all the fulfillment messages from the passed in promises, in the same order as specified (regardless of fulfillment order).
Note: Technically, the array
of values passed into Promise.all([ .. ])
can include Promises, thenables, or even immediate values. Each value in the list is essentially passed through Promise.resolve(..)
to make sure it’s a genuine Promise to be waited on, so an immediate value will just be normalized into a Promise for that value. If the array
is empty, the main Promise is immediately fulfilled.
The main promise returned from Promise.all([ .. ])
will only be fulfilled if and when all its constituent promises are fulfilled. If any one of those promises instead is rejected, the main Promise.all([ .. ])
promise is immediately rejected, discarding all results from any other promises.
Remember to always attach a rejection/error handler to every promise, even and especially the one that comes back from Promise.all([ .. ])
.
Promise.race([ .. ])
While Promise.all([ .. ])
coordinates multiple Promises concurrently and assumes all are needed for fulfillment, sometimes you only want to respond to the “first Promise to cross the finish line,” letting the other Promises fall away.
This pattern is classically called a “latch,” but in Promises it’s called a “race.”
Warning: While the metaphor of “only the first across the finish line wins” fits the behavior well, unfortunately “race” is kind of a loaded term, because “race conditions” are generally taken as bugs in programs (see Chapter 1). Don’t confuse Promise.race([ .. ])
with “race condition.”
Promise.race([ .. ])
also expects a single array
argument, containing one or more Promises, thenables, or immediate values. It doesn’t make much practical sense to have a race with immediate values, because the first one listed will obviously win — like a foot race where one runner starts at the finish line!
Similar to Promise.all([ .. ])
, Promise.race([ .. ])
will fulfill if and when any Promise resolution is a fulfillment, and it will reject if and when any Promise resolution is a rejection.
Warning: A “race” requires at least one “runner,” so if you pass an empty array
, instead of immediately resolving, the main race([..])
Promise will never resolve. This is a footgun! ES6 should have specified that it either fulfills, rejects, or just throws some sort of synchronous error. Unfortunately, because of precedence in Promise libraries predating ES6 Promise
, they had to leave this gotcha in there, so be careful never to send in an empty array
.
Let’s revisit our previous concurrent Ajax example, but in the context of a race between p1
and p2
:
// `request(..)` is a Promise-aware Ajax utility,
// like we defined earlier in the chapter
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// either `p1` or `p2` will win the race
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
Because only one promise wins, the fulfillment value is a single message, not an array
as it was for Promise.all([ .. ])
.
Timeout Race
We saw this example earlier, illustrating how Promise.race([ .. ])
can be used to express the “promise timeout” pattern:
// `foo()` is a Promise-aware function
// `timeoutPromise(..)`, defined ealier, returns
// a Promise that rejects after a specified delay
// setup a timeout for `foo()`
Promise.race( [
foo(), // attempt `foo()`
timeoutPromise( 3000 ) // give it 3 seconds
] )
.then(
function(){
// `foo(..)` fulfilled in time!
},
function(err){
// either `foo()` rejected, or it just
// didn't finish in time, so inspect
// `err` to know which
}
);
This timeout pattern works well in most cases. But there are some nuances to consider, and frankly they apply to both Promise.race([ .. ])
and Promise.all([ .. ])
equally.
“Finally”
The key question to ask is, “What happens to the promises that get discarded/ignored?” We’re not asking that question from the performance perspective — they would typically end up garbage collection eligible — but from the behavioral perspective (side effects, etc.). Promises cannot be canceled — and shouldn’t be as that would destroy the external immutability trust discussed in the “Promise Uncancelable” section later in this chapter — so they can only be silently ignored.
But what if foo()
in the previous example is reserving some sort of resource for usage, but the timeout fires first and causes that promise to be ignored? Is there anything in this pattern that proactively frees the reserved resource after the timeout, or otherwise cancels any side effects it may have had? What if all you wanted was to log the fact that foo()
timed out?
Some developers have proposed that Promises need a finally(..)
callback registration, which is always called when a Promise resolves, and allows you to specify any cleanup that may be necessary. This doesn’t exist in the specification at the moment, but it may come in ES7+. We’ll have to wait and see.
It might look like:
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
Note: In various Promise libraries, finally(..)
still creates and returns a new Promise (to keep the chain going). If the cleanup(..)
function were to return a Promise, it would be linked into the chain, which means you could still have the unhandled rejection issues we discussed earlier.
In the meantime, we could make a static helper utility that lets us observe (without interfering) the resolution of a Promise:
// polyfill-safe guard check
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// side-observe `pr`'s resolution
pr.then(
function fulfilled(msg){
// schedule callback async (as Job)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// schedule callback async (as Job)
Promise.resolve( err ).then( cb );
}
);
// return original promise
return pr;
};
}
Here’s how we’d use it in the timeout example from before:
Promise.race( [
Promise.observe(
foo(), // attempt `foo()`
function cleanup(msg){
// clean up after `foo()`, even if it
// didn't finish before the timeout
}
),
timeoutPromise( 3000 ) // give it 3 seconds
] )
This Promise.observe(..)
helper is just an illustration of how you could observe the completions of Promises without interfering with them. Other Promise libraries have their own solutions. Regardless of how you do it, you’ll likely have places where you want to make sure your Promises aren’t just silently ignored by accident.
Variations on all([ .. ]) and race([ .. ])
While native ES6 Promises come with built-in Promise.all([ .. ])
and Promise.race([ .. ])
, there are several other commonly used patterns with variations on those semantics:
none([ .. ])
is likeall([ .. ])
, but fulfillments and rejections are transposed. All Promises need to be rejected — rejections become the fulfillment values and vice versa.any([ .. ])
is likeall([ .. ])
, but it ignores any rejections, so only one needs to fulfill instead of all of them.first([ .. ])
is like a race withany([ .. ])
, which is that it ignores any rejections and fulfills as soon as the first Promise fulfills.last([ .. ])
is likefirst([ .. ])
, but only the latest fulfillment wins.
Some Promise abstraction libraries provide these, but you could also define them yourself using the mechanics of Promises, race([ .. ])
and all([ .. ])
.
For example, here’s how we could define first([ .. ])
:
// polyfill-safe guard check
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// loop through all promises
prs.forEach( function(pr){
// normalize the value
Promise.resolve( pr )
// whichever one fulfills first wins, and
// gets to resolve the main promise
.then( resolve );
} );
} );
};
}
Note: This implementation of first(..)
does not reject if all its promises reject; it simply hangs, much like a Promise.race([])
does. If desired, you could add additional logic to track each promise rejection and if all reject, call reject()
on the main promise. We’ll leave that as an exercise for the reader.
Concurrent Iterations
Sometimes you want to iterate over a list of Promises and perform some task against all of them, much like you can do with synchronous array
s (e.g., forEach(..)
, map(..)
, some(..)
, and every(..)
). If the task to perform against each Promise is fundamentally synchronous, these work fine, just as we used forEach(..)
in the previous snippet.
But if the tasks are fundamentally asynchronous, or can/should otherwise be performed concurrently, you can use async versions of these utilities as provided by many libraries.
For example, let’s consider an asynchronous map(..)
utility that takes an array
of values (could be Promises or anything else), plus a function (task) to perform against each. map(..)
itself returns a promise whose fulfillment value is an array
that holds (in the same mapping order) the async fulfillment value from each task:
if (!Promise.map) {
Promise.map = function(vals,cb) {
// new promise that waits for all mapped promises
return Promise.all(
// note: regular array `map(..)`, turns
// the array of values into an array of
// promises
vals.map( function(val){
// replace `val` with a new promise that
// resolves after `val` is async mapped
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
Note: In this implementation of map(..)
, you can’t signal async rejection, but if a synchronous exception/error occurs inside of the mapping callback (cb(..)
), the main Promise.map(..)
returned promise would reject.
Let’s illustrate using map(..)
with a list of Promises (instead of simple values):
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// double values in list even if they're in
// Promises
Promise.map( [p1,p2,p3], function(pr,done){
// make sure the item itself is a Promise
Promise.resolve( pr )
.then(
// extract value as `v`
function(v){
// map fulfillment `v` to new value
done( v * 2 );
},
// or, map to promise rejection message
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Oops"]
} );