Promise Cancellation Is Dead — Long Live Promise Cancellation!
Not very long ago Domenic Denicola (worth a follow, a very smart guy) withdrew his proposal for promise cancellation from the TC39. A lot of people might think that means that promise cancellation isn’t coming to JavaScript. Those people would be right, if you couldn’t already cancel a promise via very similar means to what was proposed.
I want to be clear: You can’t cancel a Promise. (I know, I know, mixed messages)… If something gives you a promise, your ability to cancel it has already been decided for you… you can’t. This is because promises, unlike Observables, are eager. Once you have a promise, the process that will produce that promise’s resolution is already underway, and you generally don’t have access to prevent that promise’s resolution any more. More importantly you can’t tear down resources. You must opt into cancellation the moment that the Promise is created.
So what can you do? Well, here are four options (there are more)… but you can jump to the end to find out how Promise Cancellation might have affected Rx and Observables.
Option 1 - Use Bluebird
Okay, “You can’t cancel a promise, unless it’s from Bluebird”. 😁
Bluebird has it’s own API for promise cancellation. It will give you an onCancel callback that you can use to register logic to tear down your data producer, and it also tacks a cancel
method on the promise itself you can use to cancel that promise.
// Bluebird promise cancellation
const promise = new Promise((resolve, reject, onCancel) => {
const id = setTimeout(resolve, 1000);
onCancel(() => clearTimeout(id));
});// use the promise as usual
promise.then(() => console.log('done'));// anytime later
promise.cancel();
It’s a solid and concise API that I think is simple for users to grok. The only thing I’m not sure about with it is that fact that the cancellation semantic isn’t cleanly separated from the promise.
Option 2 - Use Another Promise
The TC39 proposal was using a CancelToken. CancelToken by any other name was really just a Promise with a properly on it called reason
that you could use to synchronously check to see if you’d already been cancelled. This is something Bluebird’s approach lacks, AFAICT, but that’s not really that big of a deal, because technically, so does this approach
Either way, if you want to have a CancelToken, and you’re sad you don’t have one, you can just use another Promise. In the code below, createToken
is to make up for the lack of CancelToken.source
method to create a cancel token and it’s paired cancel function.
// a basic stitched-together CancelToken
// creation mechanism
function createToken() {
const token = {};
token.promise = new Promise(resolve => {
cancel = (reason) => {
// the reason property can be checked
// synchronously to see if you're cancelled
token.reason = reason;
resolve(reason);
});
};
return { token, cancel };
}// create a token and a function to use later.
const { token, cancel } = createToken();// your functions that return promises would also take
// a cancel token
function delay(ms, token) {
return new Promise(resolve => {
const id = setTimeout(resolve, ms);
token.promise.then((reason) => clearTimeout(id));
});
};
The above is a hacky example, to be sure, and it doesn’t include other features the proposal would add to the language (a try/catch/else semantic, for example, where cancellation would actually throw a special cancel error).
Option 3 - Use Rx Subscriptions
If you’re already using RxJS, and really, really just want a promise (I’m not sure why you would if you already have Rx, but that’s not the point). You can use Rx.Subscription to cancel a token:
// create a subscription to use with your promise
const subscription = new Rx.Subscription();const promise = new Promise(resolve => {
const id = setTimeout(resolve, 1000);
// add teardown logic to the subscription
subscription.add(() => clearTimeout(id));
});// use the promise as usual
promise.then(() => console.log('done'));// call unsubscribe at any point to cancel
subcscription.unsubscribe();
Option 4 - Ditch promises and just use Observables! (This author’s totally biased choice)
Okay, this isn’t an answer, but if you’re already using Observables, you could just use an observable, it’s not that much different from a promise, other than it’s lazy, it’s built to handle more than one value if needed, and it’s cancellable.
// create an observable, you can return teardown logic
const source$ = new Rx.Observable(observer => {
const id = setTimeout(() => observer.next(), 1000);
return () => clearTimeout(id);
};// subscribing gives you a subscription
const sub = source$.subscribe(() => console.log('done'));// cancel at any time later
sub.unsubscribe();
Just be aware that promises and observables have some semantic differences. Observables don’t multicast by default and aren’t always asynchronous. You can model a promise with an observable, but not the other way around. This is because observables are a little lower-level than a promise. Observables are really just functions that take observers, after all.
How Promise Cancellation Would Have Effected Observables
I personally liked the CancelToken approach, because it would have given JavaScript a known, ubiquitous cancellation type that we could have leveraged in RxJS. As an author of an Observable type, I can tell you that CancelToken made my job much easier, and probably would have reduced the size of the RxJS code base substantially. However, in a world where CancelToken isn’t being used with every promise, async/await function, etc, users already having access to a pre-made token would become rare, and ergonomics would suffer if we moved to a token-based model.
// current Observables
const subscription = someObservable$.subscribe(observer);
subscription.unsubscribe();// with CancelToken
const { token, cancel } = CancelToken.source();
someObservable$.subscribe(observer, token);
cancel();
In the end though, if you want cancellation, you’re probably better off with Observables. They’ve been designed for cancellation from the beginning, and they’re designed to be compatible with more different types of data producers and async sources. Observables can represent animations, user interactions (clicks, mouse moves, etc), ajax, web sockets, arrays, iterables and many, many more things. Promises are great, but you’re going to see a lot more of the benefits of same-kindedness and composition if you’re using observables than you will if you’re using Promises. But that’s probably a different post for a different time.
A little about me: I am the lead author of RxJS 5 and I run workshops on async programming and RxJS at RxWorkshop.com