Why can I not throw inside a Promise.catch handler

2019-01-03 12:25发布

Why can't I just throw an Error inside the catch callback and let the process handle the error as if it were in any other scope?

If I don't do console.log(err) nothing gets printed out and I know nothing about what happened. The process just ends...

Example:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

If callbacks get executed in the main thread, why the Error gets swallowed by a black hole?

6条回答
甜甜的少女心
2楼-- · 2019-01-03 12:29

Important things to understand here

  1. Both the then and catch functions return new promise objects.

  2. Either throwing or explicitly rejecting, will move the current promise to the rejected state.

  3. Since then and catch return new promise objects, they can be chained.

  4. If you throw or reject inside a promise handler (then or catch), it will be handled in the next rejection handler down the chaining path.

  5. As mentioned by jfriend00, the then and catch handlers are not executed synchronously. When a handler throws, it will come to an end immediately. So, the stack will be unwound and the exception would be lost. That is why throwing an exception rejects the current promise.


In your case, you are rejecting inside do1 by throwing an Error object. Now, the current promise will be in rejected state and the control will be transferred to the next handler, which is then in our case.

Since the then handler doesn't have a rejection handler, the do2 will not be executed at all. You can confirm this by using console.log inside it. Since the current promise doesn't have a rejection handler, it will also be rejected with the rejection value from the previous promise and the control will be transferred to the next handler which is catch.

As catch is a rejection handler, when you do console.log(err.stack); inside it, you are able to see the error stack trace. Now, you are throwing an Error object from it so the promise returned by catch will also be in rejected state.

Since you have not attached any rejection handler to the catch, you are not able to observe the rejection.


You can split the chain and understand this better, like this

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

The output you will get will be something like

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Inside the catch handler 1, you are getting the value of promise object as rejected.

Same way, the promise returned by the catch handler 1, is also rejected with the same error with which the promise was rejected and we are observing it in the second catch handler.

查看更多
The star\"
3楼-- · 2019-01-03 12:29

Yes promises swallow errors, and you can only catch them with .catch, as explained more in detail in other answers. If you are in Node.js and want to reproduce the normal throw behaviour, printing stack trace to console and exit process, you can do

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
查看更多
看我几分像从前
4楼-- · 2019-01-03 12:35

According the spec (see 3.III.d):

d. If calling then throws an exception e,
  a. If resolvePromise or rejectPromise have been called, ignore it.
  b. Otherwise, reject promise with e as the reason.

That means that if you throw exception in then function, it will be caught and your promise will be rejected. catch don't make a sense here, it is just shortcut to .then(null, function() {})

I guess you want to log unhandled rejections in your code. Most promises libraries fires a unhandledRejection for it. Here is relevant gist with discussion about it.

查看更多
甜甜的少女心
5楼-- · 2019-01-03 12:38

I tried the setTimeout() method detailed above...

.catch(function(err) { setTimeout(function() { throw err; }); });

Annoyingly, I found this to be completely untestable. Because it's throwing an asynchronous error, you can't wrap it inside a try/catch statement, because the catch will have stopped listening by the time error is thrown.

I reverted to just using a listener which worked perfectly and, because it's how JavaScript is meant to be used, was highly testable.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});
查看更多
够拽才男人
6楼-- · 2019-01-03 12:45

As others have explained, the "black hole" is because throwing inside a .catch continues the chain with a rejected promise, and you have no more catches, leading to an unterminated chain, which swallows errors (bad!)

Add one more catch to see what's happening:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

A catch in the middle of a chain is useful when you want the chain to proceed in spite of a failed step, but a re-throw is useful to continue failing after doing things like logging of information or cleanup steps, perhaps even altering which error is thrown.

Trick

To make the error show up as an error in the web console, as you originally intended, I use this trick:

.catch(function(err) { setTimeout(function() { throw err; }); });

Even the line numbers survive, so the link in web console takes me straight to the file and line where the (original) error happened.

Why it works

Any exception in a function called as a promise fulfillment or rejection handler gets automatically converted to a rejection of the promise you're supposed to return. The promise code that calls your function takes care of this.

A function called by setTimeout on the other hand, always runs from JavaScript stable state, i.e. it runs in a new cycle in the JavaScript event loop. Exceptions there aren't caught by anything, and make it to the web console. Since err holds all the information about the error, including the original stack, file and line number, it still gets reported correctly.

查看更多
一夜七次
7楼-- · 2019-01-03 12:54

I know this is a bit late, but I came across this thread, and none of the solutions were easy to implement for me, so I came up with my own:

I added a little helper function which returns a promise, like so:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Then, if I have a specific place in any of my promise chain where I want to throw an error (and reject the promise), I simply return from the above function with my constructed error, like so:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

This way I am in control of throwing extra errors from inside the promise-chain. If you want to also handle 'normal' promise errors, you would expand your catch to treat the 'self-thrown' errors separately.

Hope this helps, it is my first stackoverflow answer!

查看更多
登录 后发表回答