为啥我不能在 Promise.catch 处理程序中抛出?

Posted

技术标签:

【中文标题】为啥我不能在 Promise.catch 处理程序中抛出?【英文标题】:Why can I not throw inside a Promise.catch handler?为什么我不能在 Promise.catch 处理程序中抛出? 【发布时间】:2015-08-23 06:53:00 【问题描述】:

为什么我不能在 catch 回调中抛出一个 Error 并让进程处理错误,就像它在任何其他范围内一样?

如果我不这样做 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会被黑洞吞噬?

【问题讨论】:

它不会被黑洞吞噬。它拒绝.catch(…) 返回的承诺。 另见Why are exceptions used for rejecting promises in JS?和How do I handle exceptions globally with native promises in io.js / node.js? 而不是 .catch((e) => throw new Error() ),写成 .catch((e) => return Promise.reject(new Error()) ) 或简单地写成 .catch((e) => Promise.reject(new Error())) @chharvey 您评论中的所有代码 sn-ps 具有完全相同的行为,除了最初的代码显然最清楚。 【参考方案1】:

此处需要了解的重要事项

    thencatch 函数都返回新的 Promise 对象。

    无论是 throwing 还是显式拒绝,都会将当前 Promise 移至拒绝状态。

    由于thencatch 返回新的promise 对象,它们可以被链接起来。

    如果您在 Promise 处理程序(thencatch)内抛出或拒绝,它将在链接路径下的下一个拒绝处理程序中处理。

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


在您的情况下,您通过抛出 Error 对象来拒绝 do1。现在,当前的 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 被拒绝的错误相同,我们在第二个 catch 处理程序中观察到它。

【讨论】:

可能还值得补充一点,.then() 处理程序是异步的(堆栈在执行之前被展开),因此它们内部的异常必须转换为拒绝,否则将没有异常处理程序来捕获它们.【参考方案2】:

根据the spec (see 3.III.d):

d。如果调用 then 会抛出异常 e, 一种。如果已调用 resolvePromise 或 rejectPromise,则忽略它。 湾。否则以e为理由拒绝promise。

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

我猜你想在你的代码中记录未处理的拒绝。大多数承诺库都会为此触发unhandledRejection。这里是relevant gist,正在讨论它。

【讨论】:

值得一提的是unhandledRejection钩子是针对服务器端javascript的,在客户端不同的浏览器有不同的解决方案。我们尚未对其进行标准化,但它正在缓慢但肯定地实现。【参考方案3】:

正如其他人所解释的那样,“黑洞”是因为在 .catch 中抛出一个被拒绝的承诺会继续链,并且你没有更多的捕获,导致一个未终止的链,它吞噬了错误(糟糕!)

再添加一个 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!
);

当您希望链在步骤失败的情况下继续运行时,链中间的 catch 很有用,但是在执行诸如记录信息或清理步骤,甚至可能改变抛出的错误。

技巧

为了使错误在 Web 控制台中显示为错误,正如您最初的预期,我使用了这个技巧:

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

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

为什么会起作用

作为 promise 实现或拒绝处理程序调用的函数中的任何异常都会自动转换为对您应该返回的 promise 的拒绝。调用你的函数的承诺代码会处理这个问题。

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

【讨论】:

Jib,这是一个有趣的技巧,你能帮我理解为什么会这样吗? 关于那个技巧:你投掷是因为你想登录,那为什么不直接登录呢?这个技巧会在“随机”时间抛出一个无法捕获的错误......但是异常的整个想法(以及承诺处理它们的方式)是让调用者的责任捕获错误并处理它。此代码有效地使调用者无法处理错误。为什么不创建一个函数来为您处理它? function logErrors(e)console.error(e) 然后像 do1().then(do2).catch(logErrors) 一样使用它。顺便说一句,答案本身很棒,+1 @jib 我正在编写一个 AWS lambda,其中包含许多或多或少像这种情况下连接的承诺。为了在出现错误时利用 AWS 警报和通知,我需要让 lambda 崩溃引发错误(我猜)。这个技巧是获得这个的唯一方法吗? @StijndeWitt 在我的情况下,我试图在window.onerror 事件处理程序中将错误详细信息发送到我的服务器。只有通过setTimeout 技巧才能做到这一点。否则window.onerror 将永远不会听到关于 Promise 中发生的错误的消息。 @hudidit 不过,无论是console.log 还是postErrorToServer,您都可以做需要做的事情。没有理由不能将window.onerror 中的任何代码分解为单独的函数并从两个地方调用。它可能比setTimeout 行更短。【参考方案4】:

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

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

【讨论】:

不,这还不够,因为您需要将其放在您拥有的 每个 承诺链的末尾。而是hook on the unhandledRejection event 是的,这是假设您将 promise 链接起来,因此 exit 是最后一个函数,并且不会在之后被捕获。你提到的事件我认为只有在使用 Bluebird 时才会发生。 Bluebird、Q、when、native promises,……它可能会成为标准。【参考方案5】:

我试过上面详述的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;
);

【讨论】:

Jest 有 timer mocks 应该处理这种情况。【参考方案6】:

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

我添加了一个返回承诺的小辅助函数,如下所示:

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

然后,如果我在我的任何承诺链中有一个特定位置我想抛出错误(并拒绝承诺),我只需从上面的函数返回我构造的错误,如下所示:

).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 错误,则可以扩展您的 catch 以分别处理“self-thrown”错误。

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

【讨论】:

Promise.reject(error) 而不是 new Promise(function (resolve, reject) reject(error) )(无论如何都需要返回语句)【参考方案7】:
    监听未处理的错误:
window.addEventListener('unhandledrejection', e => 
  // ...
);
window.addEventListener('error', e => 
  // ...
);
    如果错误被吞没,请使用self.report(error)
.catch(error => 
  self.reportError(error);
);

https://developer.mozilla.org/en-US/docs/Web/API/reportError

【讨论】:

以上是关于为啥我不能在 Promise.catch 处理程序中抛出?的主要内容,如果未能解决你的问题,请参考以下文章

重新抛出 promise catch 中的错误

在 promise.then 或 promise.catch 中调用时,Array.push 的工作很奇怪

Javascript:如何将其他变量传递给promise .catch函数

为啥我不能使用 jQuery 从卸载事件处理程序触发 AJAX 请求?

Promise Catch 在 json rpc 调用中未收到错误

如何用 Mocha 测试 Promise catch