0

I was experimenting with this code:

let with999 = Promise.resolve(999);

let returnCatch = with999
.catch(reason => {
  console.log("catch: " + reason);
});

returnCatch.then(data => {
  console.log("then: " + data);
});

when I suddenly realized that:

The promise with999 is fulfilled and therefore the catch() method is not executed, however, the promise returned by catch() (returnCatch in this case) ends up fulfilled with the same value as with999.

So, my question is, why the catch() ends up fulfilled the returnCatch promise?

I expected returnCatch to be pending (because catch() was not executed) and consuming it with then() nothing will happen.

The same happens "doing the opposite", then() rejects a promise:

let rejected = Promise.reject(new Error('Ups!'));

let returnThen = rejected
.then(reason => {
  console.log("then: " + reason);
});

returnThen.
catch(data => {
  console.log("catch: " + data);
});

Can someone explain to me what is going on?

jwa
  • 122
  • 1
  • 7
  • Have you tried reading some documentation like https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#chaining? – deceze Jul 25 '23 at 12:13
  • Not really sure what your asking here, a Promise is either resolved or it's rejected. In your first example your instantly resolving so the catch of course should not be wating for anything, in your second example your instantly rejecting so why would you expect it to be resolved..? – Keith Jul 25 '23 at 12:15
  • 1
    Huh, this is a new take on the problem, [other people expected the returned promise to be rejected](https://stackoverflow.com/questions/16371129/chained-promises-not-passing-on-rejection)… – Bergi Jul 25 '23 at 12:32
  • "So, my question is, why the catch() ends up fulfilled the returnCatch promise?" because that's how Promises work: once a promise *resolves* to a particular value any invocation of *either* `.then` *or* `.catch` will *always* return a resolved Promise of that value. – Jared Smith Jul 25 '23 at 12:34
  • 1
    It's analogous to a `catch` statement in `try/catch`. If you don't rethrow in the `catch` statement, then the exception is considered "handled". If you want to catch the error to do something, but still further propagate the exception, then you rethrow the error in the `catch`. If not, the exception is done. Same thing with `.catch()`. It returns a new promise whose state is determined by what happens in the `.catch()` handler. – jfriend00 Jul 25 '23 at 12:56
  • 1
    Another way to put it -> `try { throw "oops" } catch (e) { }; console.log('Hello');` Promises are designed to follow try / catch logic, so if you catch, it's now your responsibility unless you re-throw. And is why that little snippet says `Hello` – Keith Jul 25 '23 at 13:00
  • Hi @Keith, I think there is something else to explain, see Bergi's answer. – jwa Jul 25 '23 at 14:04
  • Hi @jfriend00, I understand, but I think that doesn't fully answer the question. – jwa Jul 25 '23 at 14:13
  • @jwa Yes, it's a comment. Really only just meant to simplify the concept, rather than giving a full detailed answer. – Keith Jul 25 '23 at 14:43

2 Answers2

1

Why does catch() fulfill the promise that returns?

"Why" questions are always hard to answer, but it basically boils down to: because it's useful.

Maybe this is not immediately obvious from the behaviour of catch() to pass on fulfilment results, but take a look at your second example where .then() passes on the rejection: we want the .catch() callback to executed, to handle any error that did arise earlier in the promise chain. We do not want the promise chain to stop in the middle (with a promise that stays pending) because there was an error and the .then() callback was not getting executed.

So what is going on?

You already realised that this behaviour is symmetric between .then(handleResult) and .catch(handleError). But notice that these are actually just simplified syntax for .then(handleResult, null) and .then(null, handleError). The then method actually takes two parameters, one to handle fulfilment and one to handle rejection. You can (and often should) also pass both at once.

The promise returned by .then() is resolved with the result of the respective handler (or rejected if the call threw an exception), and the idea behind promise chaining is that it always becomes resolved after the original promise got settled. If the respective callback is not provided, by default the result is just passed through - whether that's .then(null, null), .then(null, handleError) on fulfilled promise, or .then(handleResult, null) on a rejected promise.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • In the first example my code is equivalent to: `with999.then(undefined, reason => { console.log("catch: " + reason);})` , consequently the following happens: "*If it is not a function, it is replaced internally by an identity function ((x) => x) that simply passes the fulfillment value forward*". [onFulfilled Optional](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#parameters), right? @Bergi – jwa Jul 25 '23 at 13:59
  • 1
    @jwa Yes that's right – Bergi Jul 25 '23 at 14:19
  • Lastly, in the second example, my code is equivalent to: `rejected.then(data => { console.log("then: " + data);}, undefined);` , consequently the following happens: "*If it is not a function, it is internally replaced with a thrower function ((x) => { throw x; }) which throws the rejection reason it received.*". [onRejected Optional](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#parameters), right? @Bergi – jwa Jul 25 '23 at 14:33
  • 1
    @jwa Same thing. – Bergi Jul 25 '23 at 14:37
  • Excellent! definitely marked as best answer @Bergi – jwa Jul 25 '23 at 14:40
-2

The code is the same as chaining .then and .catch to the initial promise. It is not the case that making a new variable for catch requires it to be rejected and then piped to the next then.

Think of it as just writing the same statement all at one go without multiple variables and the behaviour will make more sense. Since the promise is resolved the first .then will be executed, if the promise was rejected the first .catch would be executed regardless of order or declaring them or how many variables you use to do so.

Edit: This snippet is equal to above snippet, it's the same Promise being passed through.

let with999 = Promise.resolve(999).catch(reason => {
  console.log("catch: " + reason);
}).then(data => {
  console.log("then: " + data);
});
Vasundhara Mehta
  • 278
  • 2
  • 3
  • 16
  • "*the same as chaining .then and .catch to the initial promise*" - you mean like `const promise = …; promise.catch(…); promise.then(…);`? – Bergi Jul 25 '23 at 13:14
  • @Bergi i meant its similar to let with999 = Promise.resolve(999).catch(reason => { console.log("catch: " + reason); }).then(data => { console.log("then: " + data); }); It's the same promise passing through, there aren't multiple ones being created because of each new variable. – Vasundhara Mehta Jul 25 '23 at 17:00
  • Ah, yes, that's equivalent to the code with `returnCatch`. "*It's the same promise passing through*" - no it's not! `returnCatch` is a different promise than `with999`. Yes, it's not because of the variables that multiple promises are created - rather, `.then()` and `.catch()` **always** create a new promise, regardless whether you store that in a variable or immediately chain another method call to it. Using variables just gives us a name for the intermediate promise object so that we can talk about it in our explanations. – Bergi Jul 25 '23 at 18:51
  • So, are you saying that it’s a new promise entirely? And not just the .then and .catch chain based on the execution of the previous promise which gets skipped without requiring resolution or rejection of their own? – Vasundhara Mehta Jul 27 '23 at 01:01
  • Yes! If they did not create a new promise, there would be no "previous promise" and no separate "resolution or rejection of their own [promise]". Chaining does not work if it's all the same promise. – Bergi Jul 27 '23 at 09:00