0

Did some tests on my own and found that an error handler inside .then() or .catch() does NOT catch an error in 'value' of Promise.resolve(value), but DOES catch it in a Promise constructors resolve(value).

I tried both Promise.resolve and a promise constructor with resolve 'value' that throws an error (tried using an undefined variables and also an outside function that throws an error). Both had .then and .catch following.

I THINK I get the idea that in Promise.resolve, 'value' is evaluated before it is 'sent' to then - so JavaScript throws an exception and shuts it all down before it gets a chance to be caught by .then or .catch. But why does the same not happen with the resolve(value) in the promise constructor.

Just to clarify:

In the following case, JavaScript reports an unhandled exception and everything stops:


Promise.resolve(someError)
    .catch(() => {
        console.error('This never gets printed');
    })

but...

In the case below, .catch DOES catch the error and print its message:


new Promise ((resolve, reject) => {
    resolve(someError)
})
    .catch(() => {
        console.error('This actually gets printed');
    })
PurpleMongrel
  • 112
  • 1
  • 9
  • 2
    Yes, [the `Promise` constructor is throw-safe](https://stackoverflow.com/a/28692824/1048572). What exactly is your question? – Bergi Feb 25 '21 at 23:21
  • 1
    If `someError` is a function call that throws, then that is evaluated BEFORE `Promise.resolve()` is called by the interpreter. That's nothing to do with promises, that's how Javascript (or pretty much any language works). The arguments are evaluated and THEN the function is called with those arguments. So, if `someError` is a function call that throws, then you don't have anything in the code you show that catches that as it all happens before `Promise.resolve()` is called, just like any other function arguments. – jfriend00 Feb 25 '21 at 23:30
  • Should be noted that if `someError` is an `Error` object (which is the way I initially read the question) then the two cases will behave identically; `someError` will be sent down the Promise's to success path to a `.then()` if present; not to the `.catch()`. – Roamer-1888 Feb 26 '21 at 07:37

1 Answers1

2

Promise Constructor Behaviour

The syntax for calling the promise constructor is

Promise( executorFunction)

which results in Promise synchronously calling the executor function with two function arguments before returning the constructed promise. Per usual call the arguments resolve and reject for discussion purposes.

To clean up edge cases, Promise returns a rejected promise if the executor throws before calling one of its arguments, with the rejection reason of the returned promise set to the thrown error. On the other hand, if the executor calls either resolve or reject synchronously and goes on to throw an error afterwards, the returned promise will be resolved or rejected according to the argument function called: the thrown error will be ignored!

Case 1

Promise.resolve(someError)
.catch(() => {
    console.error('This never gets printed');
})

Before calling the Promise.resolve method, the JavaScript Engine evaluates the argument to pass to the method. If evaluating someError errors with a syntax or run time error, code execution stops due to the error encountered. "Everything stops", Promise.resolve is not called and no following code gets executed either.

Case 2

new Promise ((resolve, reject) => {
    resolve(someError)
})
.catch(() => {
    console.error('This actually gets printed');
})

Here someError is evaluated inside the executor as the argument to pass to resolve. Since it errors the JavaScript engine never calls resolve. But the defined behavior for Promise for the case of the executor throwing early is to return a rejected promise with reason set to the thrown error. The .catch clause in later invoked and prints 'This actually gets printed';

Standard (permalink)

If the executor throws, the ECMA Script 2015 ("ES6") standard requires the constructor to call the reject function of the returned promise in step 10 of section 25.4.3.1. However, this won't affect a promise's state if it has been synchronously resolved or rejected already: additional calls to resolve/reject functions are silently ignored if one of the pair has been called previously.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • Promises/A+ does not specify the `Promise` constructor, but only `then`. – Bergi Feb 25 '21 at 23:21
  • The A+ test suite actually uses the deferred pattern, which is not throw safe :-) – Bergi Feb 26 '21 at 00:42
  • @traktor So the diff is that resolve() is inside the executor... which has a sort of built in .then/.catch based try-catch mechanism that funnels errors as rejections into the promise chain (if none exist - I imagine JS would report error as usual). Hope I got it right. Thank you for your time - that was super helpful. – PurpleMongrel Feb 26 '21 at 01:07
  • @Bergi sorted: A+ testing uses an adapter with a user supplied `promise` constructor method. To test ES6 promises the supplied function calls `new Promise` to obtain resolve/reject functions to record in a deferred object returned - and typically never throws. The tests to make sure resolve/reject functions become inoperative after calling provided clues when reading the ECMAScript reference. Thanks again :) – traktor Feb 26 '21 at 01:34
  • @Purple_Mongrel Very close. The Promise constructor calls the executor in a `try/catch` block. If the executor throws `Promise` executes the `catch` clause of the block and tries to reject the promise about to be returned by `Promise`. This rejection will funnel the error into the promise chain and can be caught by a `catch` handler. With a rare and edge case exception: if the executor synchronously resolved or rejected the promise **before** throwing, rejecting the promise when the error is caught has no effect. – traktor Feb 26 '21 at 01:50