Reputation: 1378
While all the questions about Promise.all
focus on how to wait for all promises, I want to go the other way -- when any of the promises fails, stop the others, or even stop the whole script.
Here's a short example to illustrate:
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; });
Promise.all([promise1, promise2, promise3])
.then(values => { console.log('then', values); })
.catch(err => { console.log('catch', err); throw err; });
// results:
// > "then1"
// > "catch" "reject2"
// > "then3" <------- WHY?
The script continues to resolve promise3
, even though the final all(...).catch()
throws! Can someone explain why? What can I do to stop the other promises at the point any of them rejects?
Upvotes: 24
Views: 16035
Reputation: 465
You could implement cancellation without external libraries with AbortController
, but you would have to write your Promises a little differently.
With your promises like this:
const controller = new AbortController();
const { signal } = controller;
const promise1 = new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, 1000, 'resolve1');
signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
reject(signal.reason);
});
}).then(a => { console.log('then1'); return a; });
const promise2 = new Promise((resolve, reject) => {
const timeoutId = setTimeout(reject, 2000, 'reject2');
signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
reject(signal.reason);
});
}).then(a => { console.log('then2'); return a; });
const promise3 = new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, 3000, 'resolve3');
signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
reject(signal.reason);
});
}).then(a => { console.log('then3'); return a; });
You should validate the state of the signal before starting execution on the promise, but I left it out to be more concise.
Then you can create the following function:
/**
*
* @param {Array<Promise>} promises
* @param {AbortController} controller
*/
const promiseAllWithCancellation = async (promises, controller) => {
if (!controller) throw TypeError("AbortController is required");
try {
return await Promise.all(promises);
} catch (error) {
controller.abort();
throw error;
}
};
Then your execution would be:
promiseAllWithCancellation([promise1, promise2, promise3], controller)
.then(console.log, (error) => {
if (error instanceof Error) {
console.error(`[${error.name}]: ${error.message}`);
} else {
console.log(`[Error]: ${error}`);
}
});
// results:
// > "then1"
// > "[Error]: reject2"
If you wanted to try hard even more, you could add promiseAllWithCancellation
to the Promise
prototype or implement a custom CancellablePromise
class, inheriting from Promise
.
Upvotes: 2
Reputation: 9
If a promise is no longer reachable then the process will exit so its possible to create a little helper that achieves this
function timeoutWhen(promises, bail) {
const pending = promises
.map(promise => Promise.race([ bail, promise ]))
return Promise.all(pending)
}
const never = new Promise(() => {})
const done = Promise.resolve()
const cancel = new Promise(ok => setTimeout(ok, 1000))
timeoutWhen([ never, done ], cancel)
.then(() => {
console.log('done')
})
Will log done then exit even though the never
promise never resolves.
Upvotes: -1
Reputation: 350009
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>
Note that even though promise3 is cancelled, its setTimeout
callback will still be called. But it will not trigger the then
or catch
callbacks. It will be as if that promise never comes to a resolution ... ever.
If you want to also stop the timer event from triggering, then this is unrelated to promises, and can be done with clearTimeout
. Bluebird exposes an onCancel
callback function in the Promise constructor, which it will call when a promise is cancelled. So you can use that to remove the timer event:
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, onCancel) => { // Third argument (non-standard)
var timer = setTimeout(resolve, 3000, 'resolve3');
onCancel(_ => {
clearTimeout(timer);
console.log('cancelled 3');
});
}).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>
Upvotes: 13
Reputation: 2079
As stated in the comments promises cannot be canceled.
You would need to use a third party promise library or rxjs observables.
Upvotes: -1