Cancel Vanilla ECMAScript 6 Promise Chain

Is there a way to clean the .then instance of JavaScript Promise ?

I wrote a JavaScript test environment on top of QUnit . The structure runs the tests synchronously, running each of them in Promise . (Sorry for the length of this code block. I commented on this as best as possible, so it feels less tiring.)

 /* Promise extension -- used for easily making an async step with a timeout without the Promise knowing anything about the function it waiting on */ $$.extend(Promise, { asyncTimeout: function (timeToLive, errorMessage) { var error = new Error(errorMessage || "Operation timed out."); var res, // resolve() rej, // reject() t, // timeout instance rst, // reset timeout function p, // the promise instance at; // the returned asyncTimeout instance function createTimeout(reject, tempTtl) { return setTimeout(function () { // triggers a timeout event on the asyncTimeout object so that, // if we want, we can do stuff outside of a .catch() block // (may not be needed?) $$(at).trigger("timeout"); reject(error); }, tempTtl || timeToLive); } p = new Promise(function (resolve, reject) { if (timeToLive != -1) { t = createTimeout(reject); // reset function -- allows a one-time timeout different // from the one original specified rst = function (tempTtl) { clearTimeout(t); t = createTimeout(reject, tempTtl); } } else { // timeToLive = -1 -- allow this promise to run indefinitely // used while debugging t = 0; rst = function () { return; }; } res = function () { clearTimeout(t); resolve(); }; rej = reject; }); return at = { promise: p, resolve: res, reject: rej, reset: rst, timeout: t }; } }); /* framework module members... */ test: function (name, fn, options) { var mod = this; // local reference to framework module since promises // run code under the window object var defaultOptions = { // default max running time is 5 seconds timeout: 5000 } options = $$.extend({}, defaultOptions, options); // remove timeout when debugging is enabled options.timeout = mod.debugging ? -1 : options.timeout; // call to QUnit.test() test(name, function (assert) { // tell QUnit this is an async test so it doesn't run other tests // until done() is called var done = assert.async(); return new Promise(function (resolve, reject) { console.log("Beginning: " + name); var at = Promise.asyncTimeout(options.timeout, "Test timed out."); $$(at).one("timeout", function () { // assert.fail() is just an extension I made that literally calls // assert.ok(false, msg); assert.fail("Test timed out"); }); // run test function var result = fn.call(mod, assert, at.reset); // if the test returns a Promise, resolve it before resolving the test promise if (result && result.constructor === Promise) { // catch unhandled errors thrown by the test so future tests will run result.catch(function (error) { var msg = "Unhandled error occurred." if (error) { msg = error.message + "\n" + error.stack; } assert.fail(msg); }).then(function () { // resolve the timeout Promise at.resolve(); resolve(); }); } else { // if test does not return a Promise, simply clear the timeout // and resolve our test Promise at.resolve(); resolve(); } }).then(function () { // tell QUnit that the test is over so that it can clean up and start the next test done(); console.log("Ending: " + name); }); }); } 

If the test expires, my promise timeout will be assert.fail() in the test so that the test is marked as unsuccessful, that everything is good and good, but the test continues to work, because the Promise ( result ) test is still waiting for its solution.

I need a good way to cancel my test. I can do this by creating a field in the this.cancelTest frame module or something similar, and checking each so often (for example, at the beginning of each iteration then() ) in the test whether to cancel. However, ideally, I could use $$(at).on("timeout", /* something here */) to clear the remaining then() from my result variable, so that none of the remaining tests will run.

Is there something similar?

Quick update

I tried using Promise.race([result, at.promise]) . This did not work.

Update 2 + Confusion

To unlock me, I added a few lines with mod.cancelTest / polling in the test. (I also deleted the event trigger.)

 return new Promise(function (resolve, reject) { console.log("Beginning: " + name); var at = Promise.asyncTimeout(options.timeout, "Test timed out."); at.promise.catch(function () { // end the test if it times out mod.cancelTest = true; assert.fail("Test timed out"); resolve(); }); // ... }).then(function () { // tell QUnit that the test is over so that it can clean up and start the next test done(); console.log("Ending: " + name); }); 

I set a breakpoint in the catch expression and hit. What confuses me now is that the then() operator is not being called. Ideas?

Update 3

Thought the last. fn.call() throws an error that I did not catch, so the promise of the test was rejected before at.promise.catch() could solve it.

+83
javascript promise es6-promise cancellation
Apr 6 '15 at 20:03
source share
14 answers

Is there a way to clean .then instance of Java Promise?

No. Not at least in ECMAScript 6. Promises (and their then handlers) by default (unfortunately) are not computable. There is a bit of es-discuss discussion (like here ) about how to do it right, but whatever the way to win it, land in ES6.

The current view is that subclassing will allow you to create Promises overrides using your own implementation (not sure how well this will work).

Until the language community finds out the best way (hopefully ES7?), You can still use custom Promise implementations, many of which override the function.

The current discussion is in https://github.com/domain/cancelable-promise and https://github.com/bergus/promise-cancellation drafts.

+56
Apr 6 '15 at 20:49
source share

Although there is no standard way to do this in ES6, there is a library called Bluebird to process this file.

There is also a recommended method described as part of the response documentation. It is similar to what you have in your 2 and 3 updates.

 const makeCancelable = (promise) => { let hasCanceled_ = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then((val) => hasCanceled_ ? reject({isCanceled: true}) : resolve(val) ); promise.catch((error) => hasCanceled_ ? reject({isCanceled: true}) : reject(error) ); }); return { promise: wrappedPromise, cancel() { hasCanceled_ = true; }, }; }; const cancelablePromise = makeCancelable( new Promise(r => component.setState({...}})) ); cancelablePromise .promise .then(() => console.log('resolved')) .catch((reason) => console.log('isCanceled', reason.isCanceled)); cancelablePromise.cancel(); // Cancel the promise 

Taken from: https://facebook.imtqy.com/react/blog/2015/12/16/ismounted-antipattern.html

+46
May 27 '16 at 21:19
source share

I am really surprised that no one mentions Promise.race as a candidate for this:

 const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) }); let cancel; const cancelPromise = new Promise((resolve, reject) => { cancel = reject.bind(null, { canceled: true }) }) const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel }); 
+7
Apr 16 '18 at 20:09
source share
 const makeCancelable = promise => { let rejectFn; const wrappedPromise = new Promise((resolve, reject) => { rejectFn = reject; Promise.resolve(promise) .then(resolve) .catch(reject); }); wrappedPromise.cancel = () => { rejectFn({ canceled: true }); }; return wrappedPromise; }; 

Using:

 const cancelablePromise = makeCancelable(myPromise); // ... cancelablePromise.cancel(); 
+5
Jun 06 '19 at 9:41
source share

There are several npm libraries for revocable promises.

  1. p-cancelable https://github.com/sindresorhus/p-cancelable

  2. cancel the promise https://github.com/alkemics/CancelablePromise

+4
Nov 29 '18 at 11:46
source share

simple version :

just give out the reject function.

 function Sleep(ms,cancel_holder) { return new Promise(function(resolve,reject){ var done=false; var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms); cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();} }) } 

wraper solution (factory)

the solution found is to pass the cancel_holder object. It will have a cancel function. if it has a cancel function, then it is canceled.

This cancellation function rejects a promise with an error ("canceled").

Before allowing, rejecting or on_cancel to prevent the cancellation function, it is called for no reason.

I found it convenient to pass the cancel action to the injection

 function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) { if(!cancel_holder)cancel_holder={}; return new Promise( function(resolve,reject) { var canceled=false; var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);} var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);} var on_cancel={} cancel_holder.cancel=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; cancel_holder.canceled=true; if(on_cancel.cancel)on_cancel.cancel(); if(optional_external_cancel)optional_external_cancel(); reject(new Error('canceled')); }; return promise_fn.call(this,resolve2,reject2,on_cancel); }); } function Sleep(ms,cancel_holder) { return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){ var t=setTimeout(resolve, ms); oncacnel.cancel=function(){if(t)clearTimeout(t);} }) } let cancel_holder={}; // meanwhile in another place it can be canceled setTimeout(function(){ if(cancel_holder.cancel)cancel_holder.cancel(); },500) Sleep(1000,cancel_holder).then(function() { console.log('sleept well'); }, function(e) { if(e.message!=='canceled') throw e; console.log('sleep interrupted') }) 
+1
Sep 19 '17 at 9:40
source share

Actually, it is impossible to stop the fulfillment of a promise, but you can steal a rejection and call it from the promise itself.

 class CancelablePromise { constructor(executor) { let _reject = null; const cancelablePromise = new Promise((resolve, reject) => { _reject = reject; return executor(resolve, reject); }); cancelablePromise.cancel = _reject; return cancelablePromise; } } 

Using:

 const p = new CancelablePromise((resolve, reject) => { setTimeout(() => { console.log('resolved!'); resolve(); }, 2000); }) p.catch(console.log); setTimeout(() => { p.cancel(new Error('Fucked up!')); }, 1000); 
+1
Aug 12 '19 at 10:26
source share

If you want to stop all attempts / catches, you can do this by inserting a promise that will never be resolved. This is probably a memory leak, but it will fix the problem and should not cause too much lost memory in most applications.

 new Promise((resolve, reject) => { console.log('first chain link executed') resolve('daniel'); }).then(name => { console.log('second chain link executed') if (name === 'daniel') { // I don't want to continue the chain, return a new promise // that never calls its resolve function return new Promise((resolve, reject) => { console.log('unresolved promise executed') }); } }).then(() => console.log('last chain link executed')) // VM492:2 first chain link executed // VM492:5 second chain link executed // VM492:8 unresolved promise executed 
0
Apr 13 '17 at 17:32
source share

Here is our implementation https://github.com/permettez-moi-de-construire/cancellable-promise

Used as

 const { cancellablePromise, CancelToken, CancelError } = require('@permettezmoideconstruire/cancellable-promise') const cancelToken = new CancelToken() const initialPromise = SOMETHING_ASYNC() const wrappedPromise = cancellablePromise(initialPromise, cancelToken) // Somewhere, cancel the promise... cancelToken.cancel() //Then catch it wrappedPromise .then((res) => { //Actual, usual fulfill }) .catch((err) => { if(err instanceOf CancelError) { //Handle cancel error } //Handle actual, usual error }) 

which the:

  • Not applicable to the Promise API
  • Let's do a further undo inside a catch call
  • Rely on cancellation is rejected instead of allowed unlike any other proposal or implementation

Pulls and comments are welcome

0
Mar 26 '18 at 8:47
source share

Set the canceled property to Promise to signal then() and catch() to exit early. This is very effective, especially in Web Workers, which existing microtasks lined up in Promises from onmessage handlers.

 // Queue task to resolve Promise after the end of this script const promise = new Promise(resolve => setTimeout(resolve)) promise.then(_ => { if (promise.canceled) { log('Promise cancelled. Exiting early...'); return; } log('No cancelation signaled. Continue...'); }) promise.canceled = true; function log(msg) { document.body.innerHTML = msg; } 
0
Mar 30 '18 at 17:15
source share

@ Mikhail Yagudaev answer works for me.

But the original answer did not bind the wrapped promise with .catch () to handle the rejection, here is my improvement on top of @Michael Yagudaev's answer:

 const makeCancelablePromise = promise => { let hasCanceled = false; const wrappedPromise = new Promise((resolve, reject) => { promise .then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val))) .catch( error => (hasCanceled ? reject({ isCanceled: true }) : reject(error)) ); }); return { promise: wrappedPromise, cancel() { hasCanceled = true; } }; }; // Example Usage: const cancelablePromise = makeCancelable( new Promise((rs, rj) => { /*do something*/ }) ); cancelablePromise.promise.then(() => console.log('resolved')).catch(err => { if (err.isCanceled) { console.log('Wrapped promise canceled'); return; } console.log('Promise was not canceled but rejected due to errors: ', err); }); cancelablePromise.cancel(); 
0
Aug 03 '18 at 8:50
source share

If p is a variable containing Promise, then p.then(empty); must reject the promise when it is ultimately fulfilled or if it has already been fulfilled (yes, I know this is not an original question, but this is my question). "empty" is function empty() {} . I'm just a beginner and probably wrong, but these other answers seem too complicated. Promises should be simple.

0
Dec 20 '18 at 13:17
source share
 let r = null; let p = new Promise( (resolve,reject)=> { r = reject; setTimeout( resolve, 2000 ); }); console.log("start"); p .then ( ()=>console.log("end") ) .catch( (e)=>console.log(e) ); setTimeout( ()=>{ r("break"); }, 1000); 
0
May 15 '19 at 12:25
source share

Try the promise of rejection : https://www.npmjs.com/package/promise-abortable

 $ npm install promise-abortable 
 import AbortablePromise from "promise-abortable"; const timeout = new AbortablePromise((resolve, reject, signal) => { setTimeout(reject, timeToLive, error); signal.onabort = resolve; }); Promise.resolve(fn()).then(() => { timeout.abort(); }); 
0
Jun 28 '19 at 16:42
source share



All Articles