22

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?

quezak
  • 1,233
  • 1
  • 14
  • 30
  • 4
    Promises don’t have magic cancellation. You have to save the return value of `setTimeout` somewhere and call `clearTimeout` on it if you want that behaviour. – Ry- Feb 13 '18 at 08:52
  • 3
    But this is just an example, in the real use case my promises don't execute on timeouts only. Isn't there a general way to stop promises, other than Node's `process.exit()`? – quezak Feb 13 '18 at 08:56
  • 1
    Promises cannot be cancelled in the way that you want. That is a well known limitation. Observable libraries like rxjs offer an alternative that solve this problem. – xerotolerant Feb 13 '18 at 08:59
  • Why are you using `setTimeout` inside a `Promise`? You may find this presentation useful: https://youtu.be/cCOL7MC4Pl0 – aledpardo Feb 13 '18 at 08:59
  • @xerotolerant is right. You can't cancel a `Promise` – aledpardo Feb 13 '18 at 09:00
  • As mentioned by the others promises are not cancellable, but you can have a look on third party libraries like [**Bluerbird**](http://bluebirdjs.com/docs/features.html#cancellation-and-timeouts) – codtex Feb 13 '18 at 09:01
  • Don't execute all parallel, execute sequentially if any error comes it'll stop. promise1.then(() => { return promise2; }) .then(() => { return promise3; }) .then(() => { console.log('done'); }).catch(err => { console.log('catch', err); }); – Rahul Sharma Feb 13 '18 at 09:03
  • 1
    @aledpardo for the sake of an example :) @RahulSharma my whole point is to run a few tasks in parallel, but stop if any of them fail. Looks like without dependencies I'm stuck with `process.exit()`. – quezak Feb 13 '18 at 09:15
  • 1
    @quezak: The idea is the same as with `clearTimeout`. You need a way to stop a task, first. Promises aren’t really involved at this point. – Ry- Feb 13 '18 at 09:16
  • If you want to stick with the ES6 native Promise abstraction then you may consider turning them into cancellable promises too. Have a look at https://stackoverflow.com/a/39872802/4543207 – Redu Feb 13 '18 at 13:44
  • 1
    Guys, come on. The timeouts are there for example purposes. They just represent any asynchronous tasks, whatever those might be. Don’t you get the point? Please, do not focus on them. And to answer the question: **you got to manually introduce handlers to abort child `Promise`s.** – Константин Ван Apr 08 '22 at 17:08

3 Answers3

11

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>
trincot
  • 317,000
  • 35
  • 244
  • 286
1

As stated in the comments promises cannot be canceled.

You would need to use a third party promise library or rxjs observables.

xerotolerant
  • 1,955
  • 4
  • 21
  • 39
  • Promises don't have built in support for cancellation. However, there is the Abort Controller API that can be used to provide cancellation. Polyfill libraries are available to provide this for node and older browsers. – Adam Recvlohe Sep 14 '21 at 16:22
0

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.

fireneslo
  • 11
  • 1
  • 2