Stop other promises when Promise.all() rejects
Asked Answered
T

4

24

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?

Thesis answered 13/2, 2018 at 8:50 Comment(11)
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.Babism
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()?Thesis
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.Jolinejoliotcurie
Why are you using setTimeout inside a Promise? You may find this presentation useful: youtu.be/cCOL7MC4Pl0Lyris
@Jolinejoliotcurie is right. You can't cancel a PromiseLyris
As mentioned by the others promises are not cancellable, but you can have a look on third party libraries like BluerbirdCanuck
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); });Petaloid
@Lyris 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().Thesis
@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.Babism
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://mcmap.net/q/583785/-getting-the-latest-data-from-a-promise-returning-service-called-repeatedlyBrenna
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 Promises.Mulligatawny
I
13

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>
Insertion answered 13/2, 2018 at 9:12 Comment(0)
C
2

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.

Compost answered 4/3 at 3:12 Comment(2)
this should the accepted answer over "use a library" and no ones even mentioned promise.race or promise.allSettled. Or that you could just have method that auto rejects all if one fails in the reject callback. some here commenters even think it's not possble...Gheber
@Gheber this question is fairly old, so probably AbortController wasn't standarized yet. I just piggybacked on this question since it's the first thing that appears on SO and hopefully help any new people that get here.Compost
J
-1

As stated in the comments promises cannot be canceled.

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

Jolinejoliotcurie answered 13/2, 2018 at 9:0 Comment(1)
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.Magnitude
C
-1

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.

Camarillo answered 7/10, 2020 at 7:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.