为什么我不能扔在Promise.catch处理程序中?


127

为什么我不能只Error在catch回调内部抛出错误,让进程像在其他作用域中一样处理错误?

如果我不这样做,那么console.log(err)什么也不会打印出来,我也不知道发生了什么。这个过程刚刚结束...

例:

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
});

如果在主线程中执行了回调,为什么Error会被黑洞吞没?


11
它不会被黑洞吞噬。它拒绝了.catch(…)返回的承诺。
Bergi 2015年


而不是.catch((e) => { throw new Error() }).catch((e) => { return Promise.reject(new Error()) })还是干脆.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey注释中的所有代码段的行为都完全相同,只是最明显的是最初的代码段。
СергейГринько

Answers:


157

正如其他人所解释的,“黑洞”是因为将.catch链条丢入链中却被拒绝了,而您又没有捕获,导致链条无止境,从而吞噬了错误(不好!)。

再添加一个捕获以查看发生了什么:

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!
});

当您希望链在步骤失败的情况下继续运行时,链中间的捕获是有用的,但是在执行诸如记录信息或清除步骤之类的操作后,甚至可以更改哪个错误,重新抛出对继续失败也是有用的被抛出。

为了使错误按照您最初的意图在Web控制台中显示为错误,我使用以下技巧:

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

即使行号仍然存在,因此Web控制台中的链接将我直接带到发生(原始)错误的文件和行。

为什么运作

函数中称为承诺履行或拒绝处理程序的任何异常都会自动转换为对您应返回的承诺的拒绝。调用您的函数的promise代码将解决此问题。

另一方面,setTimeout调用的函数始终在JavaScript稳定状态下运行,即,它在JavaScript事件循环中以新的周期运行。异常不会被任何东西捕获,并使其进入Web控制台。由于err包含有关错误的所有信息,包括原始堆栈,文件和行号,因此仍可以正确报告该错误。


3
臂架,这是一个有趣的把戏,您能帮我理解为什么会起作用吗?
Brian Keith

8
关于这个窍门:您要抛出日志是因为要登录,所以为什么不直接登录呢?这个技巧将在“随机”时间抛出一个无法捕获的错误...。但是异常的整个思想(以及承诺处理它们的方式)是使调用者有责任捕获并处理错误。此代码有效地使调用者无法处理错误。为什么不仅仅创建一个函数来为您处理呢?function logErrors(e){console.error(e)}然后像这样使用它do1().then(do2).catch(logErrors)。答案本身很棒,+ 1
Stijn de Witt

3
@jib我正在编写一个AWS lambda,其中包含与本例类似或类似的许多诺言。为了在发生错误的情况下利用AWS警报和通知,我需要使lambda崩溃引发一个错误(我想)。the俩是获得此技巧的唯一方法吗?
masciugo

2
@StijndeWitt就我而言,我试图将错误详细信息发送到window.onerror事件处理程序中的服务器。只有做到setTimeout这一点,才能做到。否则,window.onerror将永远不会听到有关Promise中发生的错误的消息。
hudidit

1
@hudidit还是,console.log还是postErrorToServer,您可以做需要做的事情。没有理由将其中的任何代码都window.onerror分解为一个单独的函数并从2个地方调用。它可能比该setTimeout行更短。
Stijn de Witt'3

46

这里要了解的重要事项

  1. 无论是thencatch函数返回新承诺的对象。

  2. 抛出或显式拒绝都会将当前的承诺移至拒绝状态。

  3. 由于thencatch返回新的promise对象,因此可以将它们链接起来。

  4. 如果您在诺言处理程序(thencatch)中抛出或拒绝,它将在链接路径中的下一个拒绝处理程序中进行处理。

  5. 如jfriend00所述,thenand catch处理程序不会同步执行。当处理程序抛出时,它将立即结束。因此,堆栈将被展开,并且异常将丢失。这就是为什么抛出异常会拒绝当前的承诺。


在您的情况下,您是do1通过抛出Error对象来拒绝内部的。现在,当前的Promise将处于拒绝状态,并且控制权将被转移到下一个处理程序,then在我们的例子中。

由于该then处理程序没有拒绝处理程序,因此do2将根本不会执行。您可以通过console.log在其中使用来确认。由于当前的Promise没有拒绝处理程序,因此也会使用前一个Promise的拒绝值来拒绝它,并将控制权转移到下一个处理程序catch

catch拒绝处理程序一样,当您console.log(err.stack);在拒绝处理程序中进行操作时,您可以看到错误堆栈跟踪。现在,您正在Error从中抛出一个对象,因此所返回的承诺catch也将处于拒绝状态。

由于您尚未在上附加任何拒绝处理程序,因此catch您无法观察到拒绝。


您可以像这样拆分链并更好地理解它

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);
});

您将获得的输出将类似于

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

catch处理程序1中,您将获得promise对象的值被拒绝。

同样,catch处理程序1 返回的promise 也将promise被拒绝,并返回与被拒绝相同的错误,我们将在第二个catch处理程序中对其进行观察。


3
可能还值得补充一点的.then()是,处理程序是异步的(在执行之前堆栈已取消缠绕),因此必须将其中的异常转换为拒绝,否则将没有异常处理程序来捕获它们。
jfriend00

7

我尝试了setTimeout()上面详述的方法...

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

令人讨厌的是,我发现这是完全无法测试的。因为它引发了一个异步错误,所以您不能将其包装在一条try/catch语句中,因为在catch引发错误时,它将停止侦听。

我转而使用只工作得很好的侦听器,并且因为它是JavaScript的使用方式,因此可以高度测试。

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

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
笑话有计时器的模拟应该处理这种情况。
jordanbtucker

2

根据规范(请参阅3.III.d)

d。如果调用则引发异常e,
  a。如果已调用resolvePromise或rejectPromise,请忽略它。
  b。否则,以e为理由拒绝诺言。

这意味着,如果您在then函数中抛出异常,它将被捕获并且您的诺言将被拒绝。catch在这里没有意义,这只是通向.then(null, function() {})

我猜您想在代码中记录未处理的拒绝。大多数诺言库都unhandledRejection为此而生。这是有关此讨论的要点


值得一提的是,该unhandledRejection挂钩是针对服务器端JavaScript的,在客户端,不同的浏览器具有不同的解决方案。我们尚未对其进行标准化,但是它正在缓慢而可靠地实现。
本杰明·格林鲍姆

1

我知道这有点晚了,但是我遇到了这个线程,而且没有一个解决方案对我来说很容易实现,因此我想出了自己的解决方案:

我添加了一个小助手函数,该函数返回一个promise,如下所示:

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

然后,如果我在我的任何promise链中都有一个特定的地方要抛出错误(并拒绝promise),那么我只需从上面的函数中返回构造的错误,就像这样:

}).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);
})

这样,我就可以控制从Promise链内部抛出额外的错误。如果您还想处理“正常”承诺错误,则可以扩展捕获范围以分别处理“自我抛出”错误。

希望这会有所帮助,这是我的第一个stackoverflow答案!


Promise.reject(error)而不是new Promise(function (resolve, reject){ reject(error) })(无论如何都需要一个返回语句)
Funkodebat,

0

“是”承诺会吞下错误,并且您只能使用捕获错误,.catch如其他答案中更详细地解释的那样。如果您在Node.js中并且想要重现正常throw行为,将堆栈跟踪打印到控制台并退出过程,则可以执行

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
不,这还不够,因为您需要将其放在每个承诺链的末尾。相反,这次unhandledRejection活动吸引了人们的注意
Bergi 2015年

是的,这是假设您链接了您的promise,因此退出是最后一个功能,不会被追赶。我认为您提到的事件仅在使用Bluebird时发生。
耶苏斯·卡雷拉

Q,Bluebird,何时,原生承诺,……它可能会成为标准。
Bergi 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.