Promises
Let’s clear up some misconceptions: Promises are not about replacing callbacks. Promises provide a trustable intermediary — that is, between your calling code and the async code that will perform the task — to manage callbacks.
Another way of thinking about a Promise is as an event listener, on which you can register to listen for an event that lets you know when a task has completed. It’s an event that will only ever fire once, but it can be thought of as an event nonetheless.
Promises can be chained together, which can sequence a series of asychronously completing steps. Together with higher-level abstractions like the all(..)
method (in classic terms, a “gate”) and the race(..)
method (in classic terms, a “latch”), promise chains provide a mechanism for async flow control.
Yet another way of conceptualizing a Promise is that it’s a future value, a time-independent container wrapped around a value. This container can be reasoned about identically whether the underlying value is final or not. Observing the resolution of a Promise extracts this value once available. In other words, a Promise is said to be the async version of a sync function’s return value.
A Promise can only have one of two possible resolution outcomes: fulfilled or rejected, with an optional single value. If a Promise is fulfilled, the final value is called a fulfillment. If it’s rejected, the final value is called a reason (as in, a “reason for rejection”). Promises can only be resolved (fulfillment or rejection) once. Any further attempts to fulfill or reject are simply ignored. Thus, once a Promise is resolved, it’s an immutable value that cannot be changed.
Clearly, there are several different ways to think about what a Promise is. No single perspective is fully sufficient, but each provides a separate aspect of the whole. The big takeaway is that they offer a significant improvement over callbacks-only async, namely that they provide order, predictability, and trustability.
Making and Using Promises
To construct a promise instance, use the Promise(..)
constructor:
var p = new Promise( function pr(resolve,reject){
// ..
} );
The Promise(..)
constructor takes a single function (pr(..)
), which is called immediately and receives two control functions as arguments, usually named resolve(..)
and reject(..)
. They are used as:
- If you call
reject(..)
, the promise is rejected, and if any value is passed toreject(..)
, it is set as the reason for rejection. - If you call
resolve(..)
with no value, or any non-promise value, the promise is fulfilled. - If you call
resolve(..)
and pass another promise, this promise simply adopts the state — whether immediate or eventual — of the passed promise (either fulfillment or rejection).
Here’s how you’d typically use a promise to refactor a callback-reliant function call. If you start out with an ajax(..)
utility that expects to be able to call an error-first style callback:
function ajax(url,cb) {
// make request, eventually call `cb(..)`
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
if (err) {
// handle ajax error
}
else {
// handle `contents` success
}
} );
You can convert it to:
function ajax(url) {
return new Promise( function pr(resolve,reject){
// make request, eventually call
// either `resolve(..)` or `reject(..)`
} );
}
// ..
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
// handle `contents` success
},
function rejected(reason){
// handle ajax error reason
}
);
Promises have a then(..)
method that accepts one or two callback functions. The first function (if present) is treated as the handler to call if the promise is fulfilled successfully. The second function (if present) is treated as the handler to call if the promise is rejected explicitly, or if any error/exception is caught during resolution.
If one of the arguments is omitted or otherwise not a valid function — typically you’ll use null
instead — a default placeholder equivalent is used. The default success callback passes its fulfillment value along and the default error callback propagates its rejection reason along.
The shorthand for calling then(null,handleRejection)
is catch(handleRejection)
.
Both then(..)
and catch(..)
automatically construct and return another promise instance, which is wired to receive the resolution from whatever the return value is from the original promise’s fulfillment or rejection handler (whichever is actually called). Consider:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return contents.toUpperCase();
},
function rejected(reason){
return "DEFAULT VALUE";
}
)
.then( function fulfilled(data){
// handle data from original promise's
// handlers
} );
In this snippet, we’re returning an immediate value from either fulfilled(..)
or rejected(..)
, which then is received on the next event turn in the second then(..)
‘s fulfilled(..)
. If we instead return a new promise, that new promise is subsumed and adopted as the resolution:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return ajax(
"http://some.url.2?v=" + contents
);
},
function rejected(reason){
return ajax(
"http://backup.url.3?err=" + reason
);
}
)
.then( function fulfilled(contents){
// `contents` comes from the subsequent
// `ajax(..)` call, whichever it was
} );
It’s important to note that an exception (or rejected promise) in the first fulfilled(..)
will not result in the first rejected(..)
being called, as that handler only responds to the resolution of the first original promise. Instead, the second promise, which the second then(..)
is called against, receives that rejection.
In this previous snippet, we are not listening for that rejection, which means it will be silently held onto for future observation. If you never observe it by calling a then(..)
or catch(..)
, then it will go unhandled. Some browser developer consoles may detect these unhandled rejections and report them, but this is not reliably guaranteed; you should always observe promise rejections.
Note: This was just a brief overview of Promise theory and behavior. For a much more in-depth exploration, see Chapter 3 of the Async & Performance title of this series.
Thenables
Promises are genuine instances of the Promise(..)
constructor. However, there are promise-like objects called thenables that generally can interoperate with the Promise mechanisms.
Any object (or function) with a then(..)
function on it is assumed to be a thenable. Any place where the Promise mechanisms can accept and adopt the state of a genuine promise, they can also handle a thenable.
Thenables are basically a general label for any promise-like value that may have been created by some other system than the actual Promise(..)
constructor. In that perspective, a thenable is generally less trustable than a genuine Promise. Consider this misbehaving thenable, for example:
var th = {
then: function thener( fulfilled ) {
// call `fulfilled(..)` once every 100ms forever
setInterval( fulfilled, 100 );
}
};
If you received that thenable and chained it with th.then(..)
, you’d likely be surprised that your fulfillment handler is called repeatedly, when normal Promises are supposed to only ever be resolved once.
Generally, if you’re receiving what purports to be a promise or thenable back from some other system, you shouldn’t just trust it blindly. In the next section, we’ll see a utility included with ES6 Promises that helps address this trust concern.
But to further understand the perils of this issue, consider that any object in any piece of code that’s ever been defined to have a method on it called then(..)
can be potentially confused as a thenable — if used with Promises, of course — regardless of if that thing was ever intended to even remotely be related to Promise-style async coding.
Prior to ES6, there was never any special reservation made on methods called then(..)
, and as you can imagine there’s been at least a few cases where that method name has been chosen prior to Promises ever showing up on the radar screen. The most likely case of mistaken thenable will be async libraries that use then(..)
but which are not strictly Promises-compliant — there are several out in the wild.
The onus will be on you to guard against directly using values with the Promise mechanism that would be incorrectly assumed to be a thenable.
Promise
API
The Promise
API also provides some static methods for working with Promises.
Promise.resolve(..)
creates a promise resolved to the value passed in. Let’s compare how it works to the more manual approach:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
resolve( 42 );
} );
p1
and p2
will have essentially identical behavior. The same goes for resolving with a promise:
var theP = ajax( .. );
var p1 = Promise.resolve( theP );
var p2 = new Promise( function pr(resolve){
resolve( theP );
} );
Tip: Promise.resolve(..)
is the solution to the thenable trust issue raised in the previous section. Any value that you are not already certain is a trustable promise — even if it could be an immediate value — can be normalized by passing it to Promise.resolve(..)
. If the value is already a recognizable promise or thenable, its state/resolution will simply be adopted, insulating you from misbehavior. If it’s instead an immediate value, it will be “wrapped” in a genuine promise, thereby normalizing its behavior to be async.
Promise.reject(..)
creates an immediately rejected promise, the same as its Promise(..)
constructor counterpart:
var p1 = Promise.reject( "Oops" );
var p2 = new Promise( function pr(resolve,reject){
reject( "Oops" );
} );
While resolve(..)
and Promise.resolve(..)
can accept a promise and adopt its state/resolution, reject(..)
and Promise.reject(..)
do not differentiate what value they receive. So, if you reject with a promise or thenable, the promise/thenable itself will be set as the rejection reason, not its underlying value.
Promise.all([ .. ])
accepts an array of one or more values (e.g., immediate values, promises, thenables). It returns a promise back that will be fulfilled if all the values fulfill, or reject immediately once the first of any of them rejects.
Starting with these values/promises:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
setTimeout( function(){
resolve( 43 );
}, 100 );
} );
var v3 = 44;
var p4 = new Promise( function pr(resolve,reject){
setTimeout( function(){
reject( "Oops" );
}, 10 );
} );
Let’s consider how Promise.all([ .. ])
works with combinations of those values:
Promise.all( [p1,p2,v3] )
.then( function fulfilled(vals){
console.log( vals ); // [42,43,44]
} );
Promise.all( [p1,p2,v3,p4] )
.then(
function fulfilled(vals){
// never gets here
},
function rejected(reason){
console.log( reason ); // Oops
}
);
While Promise.all([ .. ])
waits for all fulfillments (or the first rejection), Promise.race([ .. ])
waits only for either the first fulfillment or rejection. Consider:
// NOTE: re-setup all test values to
// avoid timing issues misleading you!
Promise.race( [p2,p1,v3] )
.then( function fulfilled(val){
console.log( val ); // 42
} );
Promise.race( [p2,p4] )
.then(
function fulfilled(val){
// never gets here
},
function rejected(reason){
console.log( reason ); // Oops
}
);
Warning: While Promise.all([])
will fulfill right away (with no values), Promise.race([])
will hang forever. This is a strange inconsistency, and speaks to the suggestion that you should never use these methods with empty arrays.