3

Normally, when a Promise in JavaScript rejects without handling, we get unhandled promise rejection error.

But then what happens to all rejected promises ignored by Promise.race logic? Why don't they throw the same error?

Consider the following test:

const normal = new Promise((resolve, reject) => {
    setTimeout(() => resolve(123), 100);
});

const err = new Promise((resolve, reject) => {
    setTimeout(() => reject('ops'), 500);
});

const test = Promise.race([normal, err]);

test.then(data => {
    console.log(data);
});

The test above simply outputs 123, but no unhandled promise rejection error for our err promise.

I'm trying to understand what happens to all those rejected promises then, hence the question.

We potentially end up with a bunch of loose promises that continue running in the background, without any error handling, and never any reporting about unhandled promise rejections. This seems kind of dangerous.


Case in point. I was trying to implement combine logic for asynchronous iterables (similar to this), which requires use of Promise.race, while at the same time tracking rejections of any parameters passed into it, because the combine function needs then to reject on the next request.

vitaly-t
  • 24,279
  • 15
  • 116
  • 138
  • 3
    When you use [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) you tell the system that you only care about the first promise that is either rejected or resolved, the second one is ignored. See the documentation for details. – fredrik Dec 18 '21 at 11:49
  • 2
    "*I'm trying to understand what happens to all those rejected promises then, hence the question.*" nothing happens. With `race` you only care about the first completed promise, not the rest. So, it doesn't matter that it's rejected. Same how it doesn't matter that it is fulfilled. – VLAZ Dec 18 '21 at 11:51
  • @fredrik Promises that are ignored by `Promise.race` continue to execute, and may result in rejection. I do not see the documentation ever saying anything like `other rejected promises are just swallowed`. You effectively end up with a bunch of loose promises that continue running in the background, and without any error handling. That doesn't seem right. – vitaly-t Dec 18 '21 at 11:58
  • 2
    @vitaly-t A promise isn't "running". The tasks that would resolve the promise are still running, yes, but you can't do anything about that since there's no cancellation mechanism. – Bergi Dec 18 '21 at 12:07
  • I believe you could improve your question by showing an example that does fire the unhandled event and the same with just the Promise.race going there. No time to dig the specs here, but the logical explanation is that `Promise.race()` does internally and conceptually call both `then` and `catch`, meaning the promise is actually handled by it, even if the handler of the throwing Promise would just do nothing here (think of it as `.catch((reason) => { if(firstFired) { return; }...`. – Kaiido Dec 18 '21 at 12:40
  • Why not use Promise.all, isnt it like combine? – Yftach Dec 18 '21 at 12:42
  • @Yftach You're thinking of the wrong `combine` logic. I was talking about [this one](https://stackoverflow.com/questions/50585456/how-can-i-interleave-merge-async-iterables). – vitaly-t Dec 18 '21 at 12:45
  • 1
    @Kaiido `catch` is never called internally. It's always `.then()` with two arguments. – Bergi Dec 18 '21 at 12:45
  • @Bergi If you attach `catch` to that `err` promise, it gets caught. – vitaly-t Dec 18 '21 at 12:46
  • @vitaly-t there why would you use Promise.race for this case then? By definition the promises that "lost the race" are discarded – Yftach Dec 18 '21 at 12:49
  • @Yftach It is explained at the bottom of my question (case in point). – vitaly-t Dec 18 '21 at 12:53
  • @Bergi "and conceptually" – Kaiido Dec 18 '21 at 14:11
  • @Kaiido It seems like you have a different conceptualisation. To me (and to the spec), `then` is where the actual stuff happens, `catch` is just a convenience wrapper. – Bergi Dec 18 '21 at 14:22
  • @Bergi I only meant it to convey that the `onRejected` callback was also passed. That was colloquial talk, which I guess is fine in a comment stating "No time to dig the specs here", no? – Kaiido Dec 18 '21 at 14:52

2 Answers2

3

Normally, when a Promise in JavaScript rejects without handling, we get unhandled promise rejection error.

Yes, this happens when a promise is getting rejected that had never gotten .then() called on it to install handlers, i.e. one that is the final promise of a chain.

(Notice that .catch(onRejected) internally delegates tot .then(undefined, onRejected), so the promise is getting marked as handled the same.)

But then what happens to all rejected promises ignored by Promise.race logic? Why don't they throw the same error?

The Promise.race does call .then() on all promises in its argument, marking them as handled:

Promise.race = function(thenables) {
    return new Promise((resolve, reject) => {
        for (const thenable of thenables) {
            Promise.resolve(thenable).then(resolve, reject);
        }
    });
};

Notice it doesn't re-throw the error when the outer promise is already resolved, it's just getting ignored. This is by design: when using Promise.race, you state that you are only interested in the first result, and everything else can be discarded. Causing unhandled promise rejections from the promises that didn't win the race to crash your application would be rather disruptive.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • In my example, if we attach handlers: `err.then().catch()`, we will see that `.then` is never called, but `.catch` is called. So I don't know about `Promise.race` calling `then`, it doesn't look that way. – vitaly-t Dec 18 '21 at 12:58
  • @vitaly-t Which example? No, don't attach handlers using `p.then(onFulfilled).catch(onRejected)`, [use `p.then(onFulfilled, onRejected)` instead](https://stackoverflow.com/q/24662289/1048572). The implementation of `Promise.race` I gave is a quite literal translation of the spec text. – Bergi Dec 18 '21 at 13:01
  • The example I provided in my question, which has promise `err` that errors. I attached `then` and `catch` to it, and only `catch` is called, which is what I expected. My question arose from the fact that even though it does reject, nothing happens if I do not add `catch` to it at all. – vitaly-t Dec 18 '21 at 13:08
  • @vitaly-t The code snippet in the question has neither `err.then(…)` nor `err.catch(…)`, only `Promise.race([…, err])`, which does `err.then(…, …)` internally. Am I missing something? – Bergi Dec 18 '21 at 13:11
  • Your answer makes it sound like having `.then(cb)` is enough to prevent the unhandledrejection event to fire, it's not, you need to pass the catcher too. – Kaiido Dec 18 '21 at 14:15
  • @Kaiido It absolutely is. When doing `const p2 = p1.then(cb)`, no unhandledrejection will fire on `p1`. Of course, when `p1` rejects `p2` will also get rejected, and an unhandledrejection will fire on *that* - unless you install some further handlers via `p2.then(…)`. – Bergi Dec 18 '21 at 14:17
0

From MDN:

The Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise.

Your code fulfills because the faster promise calls resolve. Swap them around and it rejects.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);
}).catch((value) => {
  console.log('error ', value);
});
Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335