Reputation: 14318
Is there a method for clearing the .then
s of a JavaScript Promise
instance?
I've written a JavaScript test framework on top of QUnit. The framework runs tests synchronously by running each one in a Promise
. (Sorry for the length of this code block. I commented it as best I can, so it feels less tedious.)
/* Promise extension -- used for easily making an async step with a
timeout without the Promise knowing anything about the function
it's 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 a test times out, my timeout Promise will assert.fail()
on the test so that the test is marked as failed, which is all well and good, but the test continues to run because the test Promise (result
) is still waiting to resolve it.
I need a good way to cancel my test. I can do it by creating a field on the framework module this.cancelTest
or something, and checking every so often (e.g. at the beginning of each then()
iteration) within the test whether to cancel out. However, ideally, I could use $$(at).on("timeout", /* something here */)
to clear the remaining then()
s on my result
variable, so that none of the rest of the test is run.
Does something like this exist?
I tried using Promise.race([result, at.promise])
. It didn't work.
To unblock me, I added a few lines with the mod.cancelTest
/polling within the test idea. (I also removed 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
statement, and it's being hit. What's confusing me now is that the then()
statement isn't being called. Ideas?
Figured the last thing out. fn.call()
was throwing an error which I didn't catch, so the test promise was rejecting before at.promise.catch()
could resolve it.
Upvotes: 158
Views: 181378
Reputation: 4569
simple version:
just give out the reject function.
simple idea:
function MyPromise(myparams,cancel_holder) {
return new Promise(function(resolve,reject){
//do work here
cancel_holder.cancel=reject
}
}
or simple idea2:
function MyPromise() {
var cancel_holder={};
var promise=new Promise(function(resolve,reject){
//do work here
cancel_holder.cancel=reject;
}
promise.cancel=function(){ cancel_holder.cancel(); }
return promise;
}
example:
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();}
})
}
a wraper solution (factory)
the solution I found is to pass a cancel_holder object. it will have a cancel function. if it has a cancel function then it is cancelable.
This cancel function rejects the promise with Error('canceled').
Before resolve, reject, or on_cancel prevent the cancel function to be called without reason.
I have found convenient to pass the cancel action by 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')
})
Upvotes: 2
Reputation: 9295
If your code is placed in a class you could use a decorator for that. You have such decorator in the utils-decorators (npm install --save utils-decorators
). It will cancel the previous invocation of the decorated method if before the resolving of the previous call there was made another call for that specific method.
import {cancelPrevious} from 'utils-decorators';
class SomeService {
@cancelPrevious()
doSomeAsync(): Promise<any> {
....
}
}
or you could use a wrapper function:
import {cancelPreviousify} from 'utils-decorators';
const cancelable = cancelPreviousify(originalMethod)
https://github.com/vlio20/utils-decorators#cancelprevious-method
Upvotes: 0
Reputation: 1597
Using CPromise package we can use the following approach (Live demo)
import CPromise from "c-promise2";
const chain = new CPromise((resolve, reject, { onCancel }) => {
const timer = setTimeout(resolve, 1000, 123);
onCancel(() => clearTimeout(timer));
})
.then((value) => value + 1)
.then(
(value) => console.log(`Done: ${value}`),
(err, scope) => {
console.warn(err); // CanceledError: canceled
console.log(`isCanceled: ${scope.isCanceled}`); // true
}
);
setTimeout(() => {
chain.cancel();
}, 100);
The same thing using AbortController (Live demo)
import CPromise from "c-promise2";
const controller= new CPromise.AbortController();
new CPromise((resolve, reject, { onCancel }) => {
const timer = setTimeout(resolve, 1000, 123);
onCancel(() => clearTimeout(timer));
})
.then((value) => value + 1)
.then(
(value) => console.log(`Done: ${value}`),
(err, scope) => {
console.warn(err);
console.log(`isCanceled: ${scope.isCanceled}`);
}
).listen(controller.signal);
setTimeout(() => {
controller.abort();
}, 100);
Upvotes: -2
Reputation: 3471
It is actually impossible to stop the execution of the promise, but you can hijack the reject 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;
}
}
Usage:
const p = new CancelablePromise((resolve, reject) => {
setTimeout(() => {
console.log('resolved!');
resolve();
}, 2000);
})
p.catch(console.log);
setTimeout(() => {
p.cancel(new Error('Messed up!'));
}, 1000);
Upvotes: 10
Reputation: 16281
I am still working through this idea, but here is how I have implemented a cancellable Promise using setTimeout
as an example.
The idea is that a promise is resolved or rejected whenever you have decided it is, so it should be a matter of deciding when you want to cancel, satisfying the criterion, and then calling the reject()
function yourself.
First, I think there are two reasons to finish a promise early: to get it over and done with (which I have called resolve) and to cancel (which I have called reject). Of course, that’s just my feeling. Of course there is a Promise.resolve()
method, but it’s in the constructor itself, and returns a dummy resolved promise. This instance resolve()
method actually resolves an instantiated promise object.
Second, you can happily add anything you like to a newly created promise object before you return it, and so I have just added resolve()
and reject()
methods to make it self-contained.
Third, the trick is to be able to access the executor resolve
and reject
functions later, so I have simply stored them in a simple object from within the closure.
I think the solution is simple, and I can’t see any major problems with it.
function wait(delay) {
var promise;
var timeOut;
var executor={};
promise=new Promise(function(resolve,reject) {
console.log(`Started`);
executor={resolve,reject}; // Store the resolve and reject methods
timeOut=setTimeout(function(){
console.log(`Timed Out`);
resolve();
},delay);
});
// Implement your own resolve methods,
// then access the stored methods
promise.reject=function() {
console.log(`Cancelled`);
clearTimeout(timeOut);
executor.reject();
};
promise.resolve=function() {
console.log(`Finished`);
clearTimeout(timeOut);
executor.resolve();
};
return promise;
}
var promise;
document.querySelector('button#start').onclick=()=>{
promise=wait(5000);
promise
.then(()=>console.log('I have finished'))
.catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>
Upvotes: 1
Reputation: 10569
Promise can be cancelled with the help of AbortController
.
Is there a method for clearing then: yes you can reject the promise with
AbortController
object and then thepromise
will bypass all then blocks and go directly to the catch block.
Example:
import "abortcontroller-polyfill";
let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")
let example = (signal) => {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
elem.textContent = "Promise resolved";
resolve("resolved")
}, 2000);
signal.addEventListener('abort', () => {
elem.textContent = "Promise rejected";
clearInterval(timeout);
reject("Promise aborted")
});
});
}
function cancelPromise() {
controller.abort()
console.log(controller);
}
example(signal)
.then(data => {
console.log(data);
})
.catch(error => {
console.log("Catch: ", error)
});
document.getElementById('abort-btn').addEventListener('click', cancelPromise);
Html
<button type="button" id="abort-btn" onclick="abort()">Abort</button>
<div id="status"> </div>
Note: need to add polyfill, not supported in all browser.
Live Example
Upvotes: 26
Reputation: 35
Try promise-abortable: 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();
});
Upvotes: 1
Reputation: 159
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;
};
Usage:
const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();
Upvotes: 15
Reputation: 1671
If p is a variable that contains a Promise, then p.then(empty);
should dismiss the promise when it eventually completes or if it is already complete (yes, I know this isn't the original question, but it is my question). "empty" is function empty() {}
. I'm just a beginner and probably wrong, but these other answers seem too complicated. Promises are supposed to be simple.
Upvotes: 0
Reputation: 1556
There are a few npm libraries for cancellable promises.
p-cancelable https://github.com/sindresorhus/p-cancelable
cancelable-promise https://github.com/alkemics/CancelablePromise
Upvotes: 4
Reputation:
@Michael Yagudaev 's answer works for me.
But the original answer did not chain the wrapped promise with .catch() to handle reject handling, 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();
Upvotes: 0
Reputation: 828
I'm 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 });
Upvotes: 27
Reputation: 9072
Set a "cancelled" property on the Promise to signal then()
and catch()
to exit early. It's very effective, especially in Web Workers that have existing microtasks queued 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;
}
Upvotes: 3
Reputation: 3747
Here's our implementation https://github.com/permettez-moi-de-construire/cancellable-promise
Used like
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 :
catch
callPulls and comments welcome
Upvotes: 1
Reputation: 117
If you want to stop all thens/catchs from being executed you can do this by injecting a promise that will never resolve. It probably has memory leak reprocusions but it will fix the issue and shouldn't cause too much wasted 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
Upvotes: -1
Reputation: 664185
Is there a method for clearing the
.then
s 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.
Upvotes: 107
Reputation: 6129
While there isn't a standard way of doing this in ES6, there is a library called Bluebird to handle this.
There is also a recommended way described as part of the react documentation. It looks similar to what you have in your 2 and 3rd 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.github.io/react/blog/2015/12/16/ismounted-antipattern.html
Upvotes: 60