Promise - How to Force Cancel a Promise

Promise - is it possible to force cancel a promise

In modern JavaScript - no

Promises have settled (hah) and it appears like it will never be possible to cancel a (pending) promise.

Instead, there is a cross-platform (Node, Browsers etc) cancellation primitive as part of WHATWG (a standards body that also builds HTML) called AbortController. You can use it to cancel functions that return promises rather than promises themselves:

// Take a signal parameter in the function that needs cancellation
async function somethingIWantToCancel({ signal } = {}) {
// either pass it directly to APIs that support it
// (fetch and most Node APIs do)
const response = await fetch('.../', { signal });
// return response.json;

// or if the API does not already support it -
// manually adapt your code to support signals:
const onAbort = (e) => {
// run any code relating to aborting here
};
signal.addEventListener('abort', onAbort, { once: true });
// and be sure to clean it up when the action you are performing
// is finished to avoid a leak
// ... sometime later ...
signal.removeEventListener('abort', onAbort);
}

// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
try {
await somethingIWantToCancel({ signal: ac.signal });
} catch (e) {
if (e.name === 'AbortError') {
// deal with cancellation in caller, or ignore
} else {
throw e; // don't swallow errors :)
}
}


No. We can't do that yet.

ES6 promises do not support cancellation yet. It's on its way, and its design is something a lot of people worked really hard on. Sound cancellation semantics are hard to get right and this is work in progress. There are interesting debates on the "fetch" repo, on esdiscuss and on several other repos on GH but I'd just be patient if I were you.

But, but, but.. cancellation is really important!

It is, the reality of the matter is cancellation is really an important scenario in client-side programming. The cases you describe like aborting web requests are important and they're everywhere.

So... the language screwed me!

Yeah, sorry about that. Promises had to get in first before further things were specified - so they went in without some useful stuff like .finally and .cancel - it's on its way though, to the spec through the DOM. Cancellation is not an afterthought it's just a time constraint and a more iterative approach to API design.

So what can I do?

You have several alternatives:

  • Use a third party library like bluebird who can move a lot faster than the spec and thus have cancellation as well as a bunch of other goodies - this is what large companies like WhatsApp do.
  • Pass a cancellation token.

Using a third party library is pretty obvious. As for a token, you can make your method take a function in and then call it, as such:

function getWithCancel(url, token) { // the token is for cancellation
var xhr = new XMLHttpRequest;
xhr.open("GET", url);
return new Promise(function(resolve, reject) {
xhr.onload = function() { resolve(xhr.responseText); });
token.cancel = function() { // SPECIFY CANCELLATION
xhr.abort(); // abort request
reject(new Error("Cancelled")); // reject the promise
};
xhr.onerror = reject;
});
};

Which would let you do:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

Your actual use case - last

This isn't too hard with the token approach:

function last(fn) {
var lastToken = { cancel: function(){} }; // start with no op
return function() {
lastToken.cancel();
var args = Array.prototype.slice.call(arguments);
args.push(lastToken);
return fn.apply(this, args);
};
}

Which would let you do:

var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc"); // this will get canceled too
synced("/url1?q=abcd").then(function() {
// only this will run
});

And no, libraries like Bacon and Rx don't "shine" here because they're observable libraries, they just have the same advantage user level promise libraries have by not being spec bound. I guess we'll wait to have and see in ES2016 when observables go native. They are nifty for typeahead though.

Cancel a vanilla ECMAScript 6 Promise chain

Is there a method for clearing the .thens of a JavaScript Promise instance?

No. Not in ECMAScript 6 at least. Promises (and their then handlers) are uncancellable by default (unfortunately). There is a bit of discussion on es-discuss (e.g. here) about how to do this in the right way, but whatever approach will win it won't land in ES6.

The current standpoint is that subclassing will allow to create cancellable promises using your own implementation (not sure how well that'll work).

Until the language commitee has figured out the best way (ES7 hopefully?) you can still use userland Promise implementations, many of which feature cancellation.

Current discussion is in the https://github.com/domenic/cancelable-promise and https://github.com/bergus/promise-cancellation drafts.

How to cancel last Promise if not resolved?

Promises aren't cancelable as such, but are cancelled in a limited sense by causing them to be rejected.

With that in mind, cancellation can be achieved with a small amount of elaboration around Promise.race() and the promise-returning function you wish to be cancelable.

function makeCancellable(fn) {
var reject_; // cache for the latest `reject` executable
return function(...params) {
if(reject_) reject_(new Error('_cancelled_')); // If previous reject_ exists, cancel it.
// Note, this has an effect only if the previous race is still pending.
let canceller = new Promise((resolve, reject) => { // create canceller promise
reject_ = reject; // cache the canceller's `reject` executable
});
return Promise.race([canceller, fn.apply(null, params)]); // now race the promise of interest against the canceller
}
}

Assuming your http call function is named httpRequest (promise is confusing):

const search = makeCancellable(httpRequest);

Now, each time search() is called, the cached reject executable is called to "cancel" the preceding search (if it exists and its race has not already fulfilled).

// Search 1: straightforward - nothing to cancel - httpRequest(200) is called
search(200)
.then(function() { console.log('search1 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 2: search 1 is cancelled and its catch callback fires - httpRequest(2000) is called
search(2000)
.then(function() { console.log('search2 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 3: search 2 is cancelled and its catch callback fires - httpRequest(1000) is called
search(1000)
.then(function() { console.log('search3 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

If necessary, the catch callbacks can test err.message === '_cancelled_' in order to distinguish between cancellation and other causes of rejection.

Cancel JavaScript Promise

You’ve assigned a rejected promise to a variable, passed the rejected promise into your function, and assigned a resolved promise to the variable. The two values the variable takes on are unrelated, and you can’t change the state of a settled promise.

Pass in a promise you can resolve:

let cancel;
let cancelPromise = new Promise((resolve) => {
cancel = resolve;
});

example(cancelPromise).then(…).catch(…);
console.log('attempting cancellation of promise');
cancel();

function example(cancel = Promise.reject()) {    return new Promise((resolve, reject) => {        const timer = setTimeout(() => resolve('jack-jack'), 5000);        cancel.then((res) => {            clearTimeout(timer);            reject('cancelled');         }, ()=>{})    });}
let cancel;let cancelPromise = new Promise((resolve) => { cancel = resolve;});
example(cancelPromise) .then((res) => console.log('res handled:' + res)) .catch((err) => console.log('err handled:' + err));console.log('attempting cancellation of promise');cancel();

Stop other promises when Promise.all() rejects

Cancellation of promises is not included in the Promises/A+ specification.

However, some Promise libraries have such a cancellation extension. Take for example bluebird:

Promise.config({ cancellation: true }); // <-- enables this non-standard feature
const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'resolve1');}).then(a => { console.log('then1'); return a; });
const promise2 = new Promise((resolve, reject) => { setTimeout(reject, 2000, 'reject2');}).then(a => { console.log('then2'); return a; });
const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 3000, 'resolve3');}).then(a => { console.log('then3'); return a; });
const promises = [promise1, promise2, promise3];
Promise.all(promises) .then(values => { console.log('then', values); }) .catch(err => { console.log('catch', err); promises.forEach(p => p.cancel()); // <--- Does not work with standard promises });
<script src="https://cdn.jsdelivr.net/bluebird/latest/bluebird.core.min.js"></script>

PromiseKit cancel a promise

In PromiseKit 7, use func cancellize() to convert a Promise or Guarantee into a promise that can be cancelled:

currentOperation = client.load(skip: skip, query: nil)
let currentOperationCancellable = currentOperation!.then { (items) in
self.processItems(items: items, skip: skip, query: query)
}.cancellize()
currentOperationCancellable.catch { (error) in
print("failed to load items - just retrying")
self.loadIfNeeded(skip: skip, query: query, onlyInStock: onlyInStock)
}

Use func cancel() or func cancel(with:) to cancel a cancellable Promise or Guarantee:

currentOperationCancellable.cancel()
currentOperationCancellable.cancel(with: NSError(domain: "", code: 0, userInfo: nil))


Related Topics



Leave a reply



Submit