How to stop a Promise.map execution after an error with Bluebird

2.8k Views Asked by At

I've a very large amount of data to create on the database and I want to generate them in parallel with a concurrency limit. In order to do that I'm using the Promise.map method from bluebird with the concurrency limit set. However, I also need to stop the execution of the chain as soon as one of the promises fails.

This is my current code:

await Promise.map(
  entry.tags,
  async (tag) => {
    await factory(Tag)().create(tag);
  },
  { concurrency: 1000 },
).catch(() => {
  /** @todo find a better way */
  process.exit(1);
});

At the moment, as you can see, I'm using process.exit(1) but I wouldn't handle the exit code there. Instead, the Promise chain should stop instantly and the error should be thrown in order to be caught from the parent function.

With the p-map library there's the parameter "stopOnError" which work exactly as I expect, I can't find anything similar on Bluebird instead. I tried also to use the .cancel() method inside the .catch() but apparently it's not working.

1

There are 1 best solutions below

1
Jeff Bowman On

Your promise chain is stopping instantly and would throw an error to your calling function. However, two things are happening here:

  1. You're telling Bluebird to queue up to 1000 Promises in parallel, and once a Promise is created there is no meaningful way to cancel it. You may see ~999 Promises have an effect even after one fails, due to your concurrency limit of 1000.
  2. By calling .catch on your Promise, you are handling a rejection that await would otherwise convert into the error you want.

See this fiddle, as Stack Snippets doesn't support Bluebird:

(async function() {
    try {
    await Promise.map(
      seq(100),
      (i) => {
        console.log(i);
        if (Math.random() > 0.5)
          return Promise.resolve();
        else return Promise.reject();
      },
      { concurrency: 5 },
    ).catch(() => {
      console.error("catch 1");
    });
  } catch(e) {
    console.error("catch 2");
  }
})();

function seq(i) {
  return Array(i).keys();
}

As the code runs, you'll see the log for "catch 1" but not "catch 2", because the catch block causes the outer promise to resolve instead of rejecting. You'll also notice that fewer than 100 numbers are printed to output; in fact, fewer than 5 print to output, as Bluebird stops mapping as soon as possible, and in this example the Promise happens to fail fast enough that Bluebird doesn't even get the chance to fill all concurrent workers. In your real code, which is presumably asynchronous, you might see most or all of your 1000 workers start before any of them fail fast enough to cancel.

You can reduce your concurrency parameter if you want fewer attempts going at once, but again there isn't a guaranteed way to cancel Promises in Javascript. Even in Bluebird, which supports a Promise cancel method, "cancelling a promise simply means that its handler callbacks will not be called". These semantics, which the doc labels "don't care" semantics in contrast to "abort" semantics, are redundant to the semantics that Promise.map already provides you.

Regarding the catch block: If you were to remove the catch call surrounding my "catch 1", you would instead fail at "catch 2" in the outer try block—equivalent to catching the error in the parent function as you prefer. As an alternative, your Promise's catch block could re-throw the rejection reason, which would cause both "catch 1" and "catch 2" to be printed in your logs:

).catch((t) => {
  console.error("catch 1");
  return Promise.reject(t);
});