Node: 默认未处理拒绝检测行为

创建于 2015-02-13  ·  135评论  ·  资料来源: nodejs/node

随着https://github.com/iojs/io.js/pull/758/ (希望)很快登陆解决https://github.com/iojs/io.js/issues/256 ,我们很快就会有未处理的承诺拒绝通过process.on("unhandledRejection"事件向进程报告。

一个故意推迟到该问题和 PR 的问题是检测到的未处理的承诺拒绝的默认行为是什么。

需要明确的是 - 这是关于没有附加catch处理程序(或带有函数第二个参数的then处理程序)的被拒绝承诺的默认行为。

不同的方法

  • 将拒绝记录到控制台。
  • process.on("uncaughtException"抛出一个错误来处理。
  • 导致进程退出。
  • 默认情况下什么都不做。

每种方法都有几个很好的论据。 我会写下我知道的论点,并会在人们在评论中提出论点时对其进行编辑。

日志记录

  • 默认情况下没有静默失败。
  • 不会破坏任何现有代码 - 没有“惊喜”。 (参见@vkurchatkin对此的评论)
  • 在 bluebird 等几个用户级库中一直是默认设置,用户对此非常满意。

    抛出错误

  • 承诺拒绝是抛出异常的异步版本。

  • 不会忽略逻辑错误或让应用程序处于不一致的状态。
  • 误报极为罕见 - 可以说,导致它们的代码很容易被重构,不会因为太晚附加错误处理程序而不会导致它们。

    进程退出

  • 区分未捕获的异常和承诺拒绝可能很有用。

    没做什么

  • 可靠的 Promise 拒绝检测是不可判定的,如果在很晚的时间附加了 catch 处理程序,则可能会报告一个未处理的拒绝,该拒绝实际上是在稍后处理的。

  • 不会破坏任何人的任何现有工作流程。 本机承诺用户根本没有向后兼容性问题。
  • 不对人们如何使用原生 Promise 做出任何假设。

人们认为默认行为应该是什么?

discuss

最有用的评论

我仍然认为未处理拒绝的默认(但可自定义)行为是使应用程序崩溃,就像未捕获的异常一样。

如果您想在 Promise 拒绝发生后对其进行处理,您可以绑定自己的“unhandledRejection”处理程序来执行此操作。 但是,我们不能默认忽略未处理的 Promise 拒绝。 您的自定义“unhandledRejection”处理程序可以执行诸如启动 30 秒计时器之类的操作,以查看 Promise 是否最终得到处理。 但是,最终,所有的 Promise 拒绝都应该被处理......如果没有,它可能是一个错误,甚至可能是一个应该故意使应用程序崩溃的错误。

恕我直言,未处理的拒绝可能与未捕获的异常一样危险。 应用程序可能处于未定义状态。 例如,即使是 SyntaxErrors 也可能导致未处理的拒绝。

所有135条评论

不会破坏任何现有代码 - 没有“惊喜”。

实际上,登录到标准输出(或标准错误,我猜)可能会导致崩溃: https ://github.com/joyent/node/issues/6612

日志记录

恕我直言,发出一些事件已经足够记录了。 直接写入控制台很脏,不方便与自定义记录器一起使用,它可能违反应用程序日志的格式。
有时我会考虑一些内置的灵活日志记录工具(日志级别、处理程序、格式化程序等),例如 python 中的logging

抛出错误

更透明的方式。 应该抛出一个被 promise 捕获但未处理的异常。 但这是痛苦的破坏行为。 开发人员应该 promise.then(fn).then(null, noop) 显式忽略异常

导致进程退出。

uncaughtException事件处理程序不会捕获抛出的错误吗? 是不是和以前的做法一样?
我是否正确理解不同之处在于,在一种情况下,异常将直接在uncaughtException事件处理程序中发送,而在第二种情况下,异常将被重新抛出,但仍会在uncaughtException中捕获

没做什么

作为让开发人员选择如何处理未处理的承诺拒绝的变体

在这些选项中,我主张什么都不做。 最终用户可以找出他们想要的默认值,并在那里添加处理。 如果你想非常严格地捕捉它们,你可以做process.on("unhandledRejection", function (err) { throw err; }) 。 就个人而言,使用 Bluebird,我只见过一次,而且是误报。

我想强调一下,这个讨论是关于_defaults_的——所有选项都对开发人员开放,他们仍然可以决定这一切。

@Havvy感谢您的想法-我发现这些统计数据与蓝鸟用户报告的数据非常不同。 误报非常罕见,但当人们有拼写错误或传递无效的 JSON 等时,真正的肯定非常常见。

@golyshevd - 只是为了清楚 - 你是throw还是不默认?

什么都不做的问题是该功能可能不存在,因为大多数开发人员无法发现它。

恕我直言,发出一些事件已经足够记录了。 直接写入控制台很脏,不方便与自定义记录器一起使用,它可能违反应用程序日志的格式。

这正是默认的未捕获异常处理程序在减去退出进程后所做的事情......

如果没有事件处理程序,我会说发出一个事件,例如unhandledRejection并抛出。 类似于uncaughtException的工作方式。 或者,如果没有unhandledRejection的处理程序,我们可以将它作为uncaughtException发出。

至少,不要选择什么都不做。 被沉默的错误是调试的噩梦。

@本杰明格

@golyshevd - 只是为了清楚 - 你是投掷还是不默认?

我不默认发出事件,因为@petkaantonov已经实现。 它可能会简单地更改为throw之类的

process.on('unhandledRejection', function (err) {
    throw err;
});

process.on('uncaughtException', function (err) {
   log(err);
});

-1 投掷 bc 并不是每个人都同意。 我们不能破坏任何人的程序,这可能会。

+1 用于默认记录。 因为 EventEmitter 默认情况下会在超过最大侦听器数量时记录一条消息,但支持通过 setMaxListeners 等覆盖默认行为,并且由于 uncaughtException 默认情况下会导致程序崩溃,但支持通过设置侦听器来覆盖该行为,我们可以在此处默认记录并支持在用户设置 unhandledRejection 侦听器时覆盖该行为。

+1 用于在 iojs.com 的 Promises 页面顶部记录此内容


从邮箱发送

2015 年 2 月 13 日星期五上午 6:48,Dmitry [email protected]写道:

@本杰明格

@golyshevd - 只是为了清楚 - 你是投掷还是不默认?
我不默认发出事件,因为@petkaantonov已经实现。 它可能会简单地更改为throw之类的

process.on('unhandledRejection', function (err) {
    throw err;
});
process.on('uncaughtException', function (err) {
   log(err);
});

直接回复此邮件或在 GitHub 上查看:
https://github.com/iojs/io.js/issues/830#issuecomment -74264116

+1 个人投掷,但也许我不理解当前的支持问题。 如果(原生)promise 是新的——人们如何在任何程度上使用它们? 当然,这对 Promise 库的影响为零。 如果您决定升级到原生 Promise,那么您需要处理行为更改。 默认吞咽错误? 对于这样一个低级别的平台,这似乎是一个令人难以置信的讨论。

要让那些认为这是“默认”吞下错误的人清楚:不处理承诺错误与忘记检查传递给回调的err参数相同,当然我们经常看到这种情况发生在现实世界以及示例代码中。 事实是,promise 实际上有一个比回调错误更好的故事,因为由于 Promise 有两个回调(一个用于完成,一个用于拒绝),我们可以检测您是否没有处理错误,并发出一个事件。

因此,承诺会“默认”吞下错误,就像回调一样。 这是关于我们如何变得更好。

...这实际上导致了解决 OP 中提出的问题的好方法。

假设您有一种神奇的方法来检测是否有人忘记处理他们的回调的err参数,而是照常进行——无论是故意的还是因为健忘。 当这种情况发生时,您是否希望 io.js 使用这种魔力登录到控制台? 让你的程序崩溃? 没做什么?

权衡非常相似,因此我希望您对该问题的回答可以帮助您将 OP 中提出的问题置于上下文中。

假设您有一种神奇的方法来检测是否有人忘记处理回调的 err 参数

忘记处理err参数_并且_ 发生了实际情况,其中err参数不为空(即发生错误)。

顺便说一句,我喜欢这种比较。

不处理承诺错误与忘记检查传递给回调的 err 参数相同

根本不一样,回调错误总是/通常是可操作的,而承诺拒绝可能是程序员错误或操作错误。

我不同意未处理的承诺拒绝就像回调错误,错误回调模式很难不小心忘记发生的错误,因为它是回调流程的一部分,你要么得到你想要的,要么得到一个错误,但是承诺会打破这个分开(为了更好),再加上你假设回调模式节点以某种方式证明了事情应该如何的概念。 _它有效_但它不是一个理想的。

Chrome 已经在追溯处理这个问题,现在确保抛出未处理的拒绝,整个页面没有被拆除的唯一原因是因为历史上对浏览器中未处理错误的默认反应是尝试并继续前进。

Chrome 已经在追溯处理这个问题,现在确保抛出未处理的拒绝

这是不准确的。 铬日志; 它不扔。

啊,就那个细节道歉——这似乎是我上次看到的最初想法,但你显然在兔子洞里更深了:)。

在 Chrome 中不抛出/onerror 的决定是否与人们已经开始使用 Promise 的方式有关? 大概是因为并不总是正确地链接它们,即您可能会返回一个链接在“下一个滴答”而不是当前的承诺?

如果是这样,出于兴趣; 是否有任何情况意味着您无法链接当前的滴答声?

如果是这样,出于兴趣; 是否有任何情况意味着您无法链接当前的滴答声?

如果你有一个容器创建了一个打算稍后使用的 Promise,那么你不能链接当前的 tick。 但是,该容器应该只做promise.catch(() => )以便在从容器外部链接承诺之前不会出现错误。

在其他情况下,您可能会意外编写此类代码,但始终可以对其进行重构。

@domenic您可以链接到需要/需要异步附加错误处理程序的流规范的相关部分吗?

我相信这些是严重的程序员错误,所以它应该终止进程或传递给uncaughtException ,除非您更改默认行为。 我不是日志记录的忠实拥护者,因为如果您依赖stdoutstderr ,它会很吵。

我不是日志记录的忠实拥护者,因为如果您依赖 stdout 或 stderr,它会很吵。

同样,这正是默认的 uncaughtException 减去退出进程后所做的。 您是否认真建议默认退出进程而不将堆栈写入stderr? 作为旁注,如果您的标准错误很嘈杂,您应该真正修复错误:D

@petkaantonov我喜欢我的标准错误。 它一点也不吵,我想保持这种状态:D

我想一个更好的表达方式是我认为这些未处理的错误应该传递给uncaughtException ,这(基本上) console.error(err.stack); process.exit(1);除非有一个听众。 在这个特定的例子中,我们可以使用unhandledException来区分这些错误。

我没有明确说它应该console.error(err.stack); process.exit(1);但这就是我所暗示的。 不仅仅是process.exit(1) - 那绝对没用。

我认为未捕获异常的一个非常有用的特性是--abort-on-uncaught-exception标志。

拥有--abort-on-unhandled-rejection标志将满足最顽固的“崩溃优先”狂热者(比如我自己)。

这意味着我得到了我想要的最重要的东西,它是一个包含异常实际堆栈的核心转储。

:+1: 用于--abort-on-unhandled-rejection标志。 我喜欢这个主意。

我认为我们在这里还没有真正达成共识——考虑到我认为这个功能有多新,这并没有引起足够的兴趣。

我们的选择是:

  • 在 TC 会议上提出这一点,这意味着 TC 决定此默认行为。 鉴于关于钩子本身的讨论是如何进行的,似乎多个 TC 成员没有使用提供类似钩子的钩子或用户空间承诺,我认为这不是一个最佳的方法。
  • 决定暂时不使用默认行为 - 我认为给它半年时间并看看人们如何使用它会给我们提供很多信息,这表明引入默认行为的窗口正在关闭,因为它会开始破坏人们代码(假设将决定的行为可能会中断)。

我认为最现实的选择是再给它几个月,并记录将来将引入的默认行为。 你怎么认为?

我在这里的 2c 是,添加中止行为可能已经太晚了,不仅来自 io.js 的使用,而且来自现有平台的开发人员是如何被教导(或缺乏或教导)的,而且事实承诺首先出现在一个环境中在未处理的异常上不会崩溃已经污染了期望(开发人员及其实现者)。

现实情况是 _maybe unhandled?_ 拒绝的概念很糟糕,它的存在是为了支持糟糕/懒惰的编程,但也完全不可能抽象地处理.. 在这个事件回调中我要做什么? 谁知道这个承诺是什么,它可能来自 20 层深的依赖包,开发人员最终会得到ON UNHANDLED -> IF DEBUG -> LOG

我会说,我希望进行外展以阻止第三方库用毫无意义的拒绝填充事件日志,在这种情况下,无论如何您都可以使行为正确。

export class MyData {
    constructor() {
        // prefetch to provide a better user experience
        this.resultPromise = http.get('http://www.google.com');
    }
}

export class ButtonClickHandler {
    onClick(myData) {
        // display spinner in case the promise is not yet fulfilled
        showSpinner();
        myData.resultPromise.then(result => {
            // display data to user
            showResult(result);
        }, error => {
            // display error to user
            showError(error);
        });
    }
}

在上面的示例中,假设MyData是在应用程序启动期间构建的,并且 Web 请求立即启动,可能在合理的 UI 向用户呈现错误之前。 在未来的某个时刻,用户单击某个按钮并调用onClick 。 在那个时间点,检查承诺的结果。

我认为这是一个完全合理的用例,通常是一个很好的做法。 Promise 只是一个存储未来结果或失败的对象。

许多人在对异常的承诺中存在相关错误,我相信这就是问题的根源。 Promise 更类似于 if 块而不是异常。 你基本上是在说,“一旦这个承诺被履行,运行两个代码路径之一。你不是说,”一旦这个承诺被履行,运行这个代码路径或抛出一个异常。“如果你想要异常行为,那么承诺是不是正确的答案。你想要一些其他的结构,虽然相似,但不会保留被拒绝的承诺。

@Zoltu这是一个非常合理的用例,并且已经讨论了很多。 您可以通过将 noop 处理程序附加到您的承诺来抑制全局拒绝处理程序:

export class MyData {
    constructor() {
        // prefetch to provide a better user experience
        this.resultPromise = http.get('http://www.google.com');
        this.resultPromise.catch(() => {})
    }
}

啊,好消息。 我一直在 Internet 上查看 ES6 承诺的各种实现,到目前为止, io.js是第一个提供了一种方法来覆盖此行为的方法。 做得好!

许多人在对异常的承诺中存在相关错误,我相信这就是问题的根源。 Promise 更类似于 if 块而不是异常。 你基本上是在说,“一旦这个承诺被履行,运行两个代码路径之一。你不是说,”一旦这个承诺被履行,运行这个代码路径或抛出一个异常。“如果你想要异常行为,那么承诺是不是正确的答案。你想要一些其他的结构,虽然相似,但不会保留被拒绝的承诺。

这也是非常错误的,整个 Promise 的 50% 是 Promise中的错误与异常的工作方式相同。

@petkaantonov你的观点似乎证实了我已经正确地确定了两个阵营之间的分界线。

我相信 async/await 会更好地解决您要解决的问题。 您可以像编写同步代码一样编写 aync 代码,并拥有 catch 块和最后机会异常处理程序。

我已经阅读了您链接的文章,但我并没有从中得到与您相同的东西。

在 async/await 距离不远的情况下,让 Promise 解决成功/错误时分支/链接的问题,而 await 可以解决 anync 异常处理的问题似乎是明智的。 如果使用最后机会处理程序来实现承诺,那么我们最终将得到两种解决方案来解决任何异常处理问题,而没有解决其他问题。

如果没有出现anync,我会更不得不说promise 应该提供最后机会处理程序,并且没有其他问题的选项,尽管似乎有两种不同的构造空间。

似乎我们被迫停滞不前没有默认行为。

我不认为这是一个好主意:-)

@vkurchatkin好的 - 你能把它提上 TC 议程吗:D?

我们需要弄清楚我们在提议什么。 console.error似乎是最安全的选择

console.error是我绝对赞成的——问题在于达成共识。

@benjamingr你能提交 PR 吗?

问题不在于对这种行为进行 PR - 它首先就它达成一致 - 对此肯定没有共识 - 而且 - 你是那个说日志记录可能导致崩溃/意外行为的人。

@vkurchatkin如,在原始 PR 中确实有一个位置用于此https://github.com/nodejs/io.js/blob/5759722cfacf17cc79651c81801a5e03521db053/src/node.js#L490 -L491

是的,这绝对需要讨论和同意。

我认为以下是可以接受的:

  • 默认未处理的拒绝处理程序_和_拒绝处理的处理程序,_log_(不抛出)配对消息。 when.js 这样做:
Potentially unhandled rejection [1] Error: this rejection will be handled later
    at /Users/brian/Projects/cujojs/when/experiments/unhandled.js:4:8
    at tryCatchReject (/Users/brian/Projects/cujojs/when/lib/makePromise.js:806:14)
    at FulfilledHandler.when (/Users/brian/Projects/cujojs/when/lib/makePromise.js:602:9)
    at ContinuationTask.run (/Users/brian/Projects/cujojs/when/lib/makePromise.js:726:24)
    at Scheduler._drain (/Users/brian/Projects/cujojs/when/lib/scheduler.js:56:14)
    at Scheduler.drain (/Users/brian/Projects/cujojs/when/lib/scheduler.js:21:9)
    at process._tickCallback (node.js:419:13)
    at Function.Module.runMain (module.js:499:11)
    at startup (node.js:119:16)
    at node.js:906:3

... one second later ...

Handled previous rejection [1] Error: this rejection will be handled later

[1] 是拒绝 ID。

  • 用于投掷的--abort-on-unhandled-rejection标志。
  • 没有默认行为的--ignore-unhandled-rejection标志。

(您可以将这两个标志合并为一个或以不同的方式命名它们;我没有强烈的意见。)

为什么我们需要标志@domenic? 如果有人想绕过默认行为,他们只需要添加一个处理程序?

至于默认行为 - 我认为以这种格式登录是一个非常合理的默认值。

这是一种方便。 我们正在打包人们可能通常想要使用的三个潜在默认值:nothing、log 和 abort。 我们允许人们通过 CLI 标志选择应用哪一个,而不必修改他们的代码。

编辑:在电话上发表评论是个坏主意。

@domenic --abort-on-unhandled-rejection不确定

你会期望像 abort on uncaught exception 这样的语义。
即 Reject(e) 需要用有意义的堆栈抛出和中止。

我想阅读核心文件。 探索真正的堆栈。 我不想看到一个字符串堆栈跟踪,而是一个真正的堆栈,我可以在其中确切地拒绝这个你处理的承诺。

只是在堆栈有两帧的下一个滴答中抛出一个错误是没有用的。

如果我们不能实现这个中止语义,那么我们应该有一个不同的标志(即--throw-on-unhandled-rejection )。
即使我们中止了,我仍然想要一个未处理的标志。 中止是为了生产。 投掷用于测试和本地开发。

回复:使用console.error() 。 可能最好使用process._rawDebug()

用于投掷的--abort-on-unhandled-rejection标志。

然后我会期待--throw-on-unhandled-rejectionabort表示核心转储,但不保证会发生这种情况。

我没有意识到这种差异。 多谢你们。

关于这件事的讨论似乎又平息了。 我在微积分上可能是错的,但看起来这个时间越长,就越难决定该行为应该是什么都不做。 标记这个tsc-agenda以期做出决定是否为时过早?

@Trott好主意! 我绝对赞成重新开始讨论

似乎任何决定都会让某人感到不安。 我认为最好的做法是做到这一点,以便用户可以以某种方式覆盖所做的任何决定,最好是在每个应用程序级别(而不是每次运行)。 我的应用程序可以在启动期间设置的全局标志就是一个简单的例子。

就个人而言,我认为默认行为应该完全符合规范,您必须选择加入不属于规范的行为。 话虽如此,扩展支持最简单的 Promise 使用是默认的,并且以高级方式利用 Promise 的用户可以选择退出该行为。

根据我对开发政策的最大努力,我用tsc-agenda对其进行了标记,希望能做出某种决定。 (希望我没有做错,如果我做错了,那么对于更了解这个的人来说应该很容易取消标记。)

我最好的猜测是,在这么晚的时候,“什么都不做”是显而易见的选择,因为它基本上保证不会破坏生态系统中的任何东西。 但我不反对记录警告而不是什么都不做。 但似乎每周都没有做出决定,这使得选择一个不是“什么都不做”的选项变得越来越难。 因此,最好在此处就前进的道路达成某种官方共识。

FWIW,我支持“什么都不做”并关闭问题(出于上述原因),将“记录警告”作为我的第二选择。

编辑:哦,是的,基于 CLI 标志和/或其他选项设置的行为变化,非常棒。 但是这个问题是关于 _default_ 行为的,而且,IMO,我认为默认选项应该是什么都不做或记录一个警告。

@小跑+1。 我认为在这种情况下发出一次性消息将是一个很好的默认行为。

@Trott大多数 TSC 对这个问题都不太适应,所以我们需要一些选项来决定。 :)

主题背景阅读材料:
https://github.com/promises-aplus/unhandled-rejections-spec/issues/1

对每项进行讨论的提案清单:
https://github.com/promises-aplus/unhandled-rejections-spec/issues

就个人而言,我建议等到 Promise 规范做出决定后再效仿。 不幸的是,他们和其他人一样停滞不前。 我相信部分原因是因为一个阵营明确希望保持不变,而另一个阵营想要改变。 一个变革阵营总是很难战胜一个明确想要不改变的阵营。

完全披露:我强烈支持no-change阵营。

@Fishrock123我不介意参加会议并解释替代方案(在本期的第一篇文章中列出)及其注意事项。 或者,我确信@petkaantonov@domenic也能够做到这一点,并且比我更深入地理解这个主题。

另一件要考虑的事情是浏览器也在努力规范这一点,回想起来不等待他们达成共识是一个很好的选择,我个人非常喜欢这些钩子,它们一直是一个“杀手级功能”说服人们在与人交谈时从 Node 0.12 迁移到 io.js。

日志记录对于不了解钩子的用户来说非常有价值,这就是浏览器所做的(尽管浏览器还没有钩子)。 我个人崩溃了,但我认为这“不适合所有人”。 由于这可以在用户空间中解决,因此无更改也是有意义的,并且是一种可行的替代方案。

只是为了将我的 2c 添加到此,我在生产应用程序中广泛使用 Promise,并且根据我的经验,异步注册错误处理程序是一种新事物。 其他人可能对此有不同的看法,但这是我的。 鉴于这一点以及良好的默认值很重要的事实:

+1 记录或 uncaughtException
-1 在退出过程中,这样做比 uncaughtException 没有优势
-1 什么都不做,这是一个可怕的默认值。 如果您不想听到非常可能的错误(至少在我的 exp 中),请明确将其关闭。

在这个阶段,TSC 很可能只是听从@domenic对这件事的看法。 除非我们能够对我们应该讨论和同意的内容有一个清晰的轮廓,否则我认为这不是一个非常有建设性的 TSC 讨论。

我目前的立场(听起来固执己见,但我试图对此保持开放的态度),我怀疑 TSC 上的其他人也会类似:

  • 如果您使用 Promises,那么您选择的范例是错误处理是二等的,这是您的选择,您应该在进入时意识到这一点。如果您不打算处理错误,那么您也在做出选择. 这与使用流或 EventEmitter 而不是处理'error'非常相似。
  • _fix_ Promises 存在的问题不是 Node 的工作,这要么是语言级别的讨论,要么是选择更好的抽象。 我们能够而且应该防范足枪的程度是有限度的。 与流和 EventEmitter 不同, Promise对象不是 Node-native,因此我们不能对其应用相同的实现推理(即,我们拥有 Promise 的方式与拥有流和 EE 的方式不同) .
  • 当我们合并 unhandledRejection 行为时,我们使用最小占用空间的方法来完成它,如果有意义的话,可以选择在将来更改它
  • 在任何领域(浏览器、Promises 实现库)似乎都没有(据我所见)出现一个新兴协议,让 Node 选择一方可能会让我们在未来的协议中处于错误的一边_应该_如何处理.

对我来说,我预计会因非承诺而崩溃的错误也会因承诺而崩溃,我也怀疑普通用户也可能会崩溃。

话虽如此,我想我在这里和@rvagg 在一起。 如果我们不打算碰它,我们需要确保我们的文档清楚unhandledRejection存在。 (他们可能已经是了?)

@Fishrock123你不能将这种推理直接应用于 Promises,因为你没有明确的“这个错误没有被处理”的情况,它_可能_在未来的某个时候被处理,而且我们选择何时触发是相当随意的一个unhandledRejection ,为什么不在 10 个滴答声后做呢? 为什么不是 10 秒后? 一分钟? 申请结束??? 它的原始 PR 有possiblyUnhandledRejection IIRC,这在技术上更准确。 因此,如果您不知道错误将不会被处理,那么选择在错误发生后崩溃的时间点几乎完全是任意的!

是的,我读到“我希望一个会因非承诺而崩溃的错误也会因承诺而崩溃”,因为根本没有崩溃,因为当您执行fs.readFile(..., function (err, result) { /* ignore err */ })时没有任何崩溃。

它的原始 PR 可能有 UnhandledRejection IIRC,这在技术上更准确。 因此,如果您不知道错误将不会被处理,那么选择在错误发生后崩溃的时间点几乎完全是任意的!

哦。 Promise 很复杂,取决于真正理解这个疯狂概念的人。 :P

我想@domenic的解释是有道理的。

@domenic的比较是恰当的。 在这两种情况下,用户都可以使用错误而不抛出错误,用户可以忽略它。 唯一的区别是可以检索错误的范围。

我刚刚想到的还有另一个差异。 Promise 执行器同步运行,但也有一个隐式的 try/catch。

在执行后同步运行的节点函数中允许传播异常。 这提出的问题是,AFAIK,无法区分执行程序中的代码何时抛出,因为它行为不端和何时调用拒绝回调。

我没有关于应该如何处理的意见。 只是想指出“从不抛出”并不是一个完全有效的比较。

我认为“我们必须做点什么”的想法可能为时已晚。 它已经发布并且已经做了一些开发人员所依赖的事情,并且在更高程度上,开发人员已经在使用原生承诺之前使用了承诺。

我看不到这里的行为发生变化,TC 对应该发生的事情太没有信心了,一旦第一个更改行为的错误报告出现,您就会将其恢复。

我在这里使用 Promise 的经验是,与回调样式错误不同,我们可以非常强烈地估计,如果 Promise 未处理一段时间,您的应用程序中就有错误。

这种“这里可能存在错误”逻辑通常存在于 IDE / 编译器中。 但正如我们所知,node 在这里是不寻常的并且对那些没有要求,这可能表明运行时处理它们,但我谨慎地说这是真的,因为我希望运行时尽可能快。

也许这又回到了 JavaScript 需要一些好的静态分析工具。

无论如何,我离题了,我的观点是,用钩子默默地失败似乎是最有可能的结果。

对我来说,这基本上归结为 TSC 对在没有用户选择加入的情况下将内容打印到 stdout/stderr 的感觉。如果他们认为这很好,那么也许我们可以添加一个默认处理程序来记录(但不会崩溃或抛出或任何东西) . 如果他们不这样做,我们应该保持原样。 我想我们可以在 TSC 会议期间讨论一下。

这在上周的会议上已经讨论过了。

行动:现在什么都没有,也许如果 v8 添加了一个钩子,用于当拒绝被垃圾收集时。

即,当未处理的拒绝被垃圾收集时,这将是采取行动(某种形式)的更理想时间,因为它比我们现在拥有的要少得多,而且由于 GC 提供的清晰度,它更容易证明行动的合理性。 据@domenic我认为,V8 似乎很快就会包含这些钩子。

稍微澄清一下。 V8 已经为我们提供了我们需要的所有钩子。 我们有责任调查各种可能性。 应该可以监视未处理的被拒绝的承诺,并向它们添加一个弱回调,以检查它们在 GC 时是否仍然未处理,完全来自 io.js 代码库。

如果有人想接手那个项目,肯定会有 TC 的兴趣。

我认为依赖 GC 来处理特定的行为可能很有用,但是很多人会反对基于 GC 等非确定性的东西采取行动。 我怀疑其中有@erights

如果该操作正在记录调试帮助,则没有问题。 这不会是用户可挂钩的。

如果它不是用户可挂钩的,我完全可以接受,我认为它甚至会非常有用:)

首先,如果它只能由具有观察日志的特权能力的人观察,我可以将其作为第一步。 但我们不应止步于此。 一般来说,充分授权的用户模式代码应该能够以自包含的方式模拟平台贡献的许多东西。 我意识到这通常是模糊的,因为我们需要逐案检查。

对于这种情况, http://wiki.ecmascript.org/doku.php?id=strawman :weak_references 和http://wiki.ecmascript.org/doku.php?id=strawman :weak_refs ,一旦统一,将允许这样做通过访问特殊 makeWeakRef 功能的用户代码发生依赖于 GC 的日志记录。 如那些页面所述,我们故意推迟提供弱引用,直到我们对模块系统有足够的经验,知道哪些模式适合仅向特权方提供某些模块。

因此,在此之前仅在调试器/控制台/日志级别提供此信息似乎是一个很好的步骤。

我对默认行为的直觉反应是,只要未处理被拒绝的承诺并且没有“unhandledRejection”事件处理程序,就会抛出异常。 这会向 stderr 打印一个很好的堆栈跟踪,并以退出代码 1 终止 Node ......这与不使用 Promises 的行为一致。

就个人而言,这是我在了解“unhandledRejection”和“rejectionHandled”事件之前所期望的默认行为。 现在我知道了它们,在这里提出一个未捕获的异常仍然是个好主意。 恕我直言,Promise.then 中的拼写错误或语法错误应该是未捕获的异常(除非由“unhandledRejection”事件处理程序显式处理)。

考虑这个例子:

var promise = new Promise(function(resolve) {
    kjjdjf();
});

在 Node 4.0.0 中会发生什么? 程序默默地继续。 如果您是具有 Promises 的 n00b(如我),“unhandledRejection”将被忽略,您的程序将处于未定义状态。

这与写作有何不同: kjjdjf(); ??? 好吧,它不是由一个 Promise 封装的! 不写promise.catch()就像忘记写try {} catch(e) {}一样。 恕我直言,只要有未处理的拒绝且没有注册的“unhandledRejection”事件处理程序,就会抛出未捕获的异常。

我刚刚在这里提交了我的想法: https ://github.com/sindresorhus/loud-rejection/pull/4

它的作用如下; 每当一个没有处理失败的承诺被垃圾收集时,它都会抛出一个错误。

我认为这是一个很好的方法,因为它允许以下内容:

var p = new Promise(function (resolve, reject) {
  setTimeout(reject, 20, new Error('unicorn'))
})

setTimeout(function () {
  p.catch(function () {
    console.log('I caught you')
  })
}, 140)

但它实际上捕捉到了这样的东西:

new Promise(function (resolve, reject) {
  setTimeout(reject, 20, new Error('unicorn'))
})

我很乐意将其视为标准行为。 你们有什么感想?

@LinusU - 我不明白这与未处理的拒绝有什么关系。 在您的第一个示例中,在调用reject之后调用p.catch ,此时仍然是未处理的拒绝,稍后由p.catch处理。 怎么说p.catch曾经被调用过? 如果p.catch只被有条件地调用怎么办?

无论如何,我仍然不明白为什么默认情况下未处理的拒绝不会引发异常......也许有人可以向我解释一下???

@bminer关键是,只要p超出范围,就会引发异常,这样就总是会抛出异常,即使p.catch只是有条件地调用。

如果我们默认抛出一个异常,下面的代码就会崩溃,即使它是完全有效的。

function SQLConnection () {
  this.connection = new Promise(function (resolve, reject) {
    setTimeout(reject, 10, new Error('Server error'))
  })
}

SQLConnection.prototype.query = function (query, cb) {
  this.connection.then(function (c) {
    // run query on c
  }, function (err) {
    cb(err)
  })
}

// ------

var c = new SQLConnection()
setTimeout(function () {
  c.query('SELECT 1', function (err) {
    console.log('The error', err)
  })
}, 600)

这就是为什么我们需要它,以及为什么我们并不总是引发异常。

在 GC 时记录绝对比在下一个滴答或类似情况下记录要好。 然而,它仍然假设_每个人_都关心拒绝。 根据您是否将承诺拒绝视为异常或错误返回,这不是一个有效的假设。

// don't care about the results on this invocation, it is a best effort reporting system
httpClient.fetchAsync("http://www.example.com/marketing");

// care very much about the results on this, we have fallback behavior that runs on failure
httpClient.fetchAsync("http://www.example.com/feature").catch(error => handleError(error));

在第一种情况下,如果我从端点收到错误,我会收到未处理的拒绝。 但是,我不在乎它是否出错,这是最好的努力。 广告也是如此,如果我无法在页面加载时获得广告,我可能不在乎,并且捕获错误将是不正确的行为(除了使未处理的拒绝静音)。

在第二种情况下,因为我关心结果,所以没有捕捉到错误。

如果您将被拒绝的 Promise 视为 _exception_,那么 GC 时的日志消息是合适的。 如果您将被拒绝的承诺视为错误返回,那么它是不合适的。

let result = httpClient.fetchAsync(...);
if (result is Error) {
    // handle error
} else {
    // process result
}

对比

try {
    let result = httpClient.fetchAsync(...);
    // process result
} catch {
    // handle error
}

@LinusU - 你的例子是完全有效的。 也就是说,代码可以正常运行,但仍会导致 _unhandled_ 拒绝。 这里的重点是确定未处理拒绝的默认行为。 您仍然可以编写自己的“unhandledRejection”事件处理程序并做任何您想做的事情。 更好的是,您可以写this.connection.catch(...)并正确处理拒绝。 但是,默认情况下,我认为您的代码应该在调用reject并且承诺在下一个滴答时没有拒绝回调时引发异常。 在 GC 时做一些事情可能会令人困惑和不可预测。

承诺拒绝让我想起了例外。 当调用reject时,表示某操作失败; 您很可能想要处理这种情况。 如果你不处理 Promise 拒绝案例,我什至会大胆质疑为什么你会首先使用 Promises。 如果您真的不关心错误,简单的p.catch(function nop() {})是忽略拒绝的好方法。 恕我直言,这类似于编写一个空的catch(e) {}块。

重申一点......未处理的拒绝感觉很像未捕获的异常; 它们是您忘记处理的错误(不是事实,只是我的看法)。 如果发生未处理的拒绝,您总是可以在“unhandledRejection”事件处理程序中很好地处理它,或者您可以修改代码以将拒绝事件处理程序添加到 Promise。 但是,默认情况下,我认为应该将未处理的拒绝视为未捕获的异常。 如果发生未处理的拒绝并且没有“unhandledRejection”事件处理程序,则会抛出未捕获的异常,“uncaughtException”处理程序将被触发,如果没有,程序将崩溃并打印堆栈跟踪。

现在,我知道未捕获的异常要严重一些,因为它们会破坏整个堆栈跟踪并使程序处于未定义状态; 然而,以这种方式拒绝的 Promise 并不危险。 不过,我相信这种默认行为为不熟悉 Promises 的程序员提供了保障。 如果他们想覆盖默认行为并进入稍微危险的领域,他们可以自由添加“unhandledRejection”事件处理程序等。

现在,我知道未捕获的异常要严重一些,因为它们会破坏整个堆栈跟踪并使程序处于未定义状态; 然而,以这种方式被拒绝的 Promise 并不危险

这不是真的,如果您在then处理程序中有未处理的异常,它将被转换为拒绝。 因此,有可能使程序处于未定义状态。 就像您尝试/捕获每一段代码一样。

对于大多数常规用户代码,我并没有真正购买整个“未定义状态”的东西——我认为在大多数情况下,你首先会使用 Promise,让节点处于未确定状态是非常困难的 :)

@bminer问题是我的代码是有效的,但是如果我们开始抛出未处理的拒绝,它就会崩溃。 我想等到用户尝试运行.query ,然后将错误返回给用户。 这就是为什么我相信在垃圾收集期间绝对是我们可以做到这一点的最早时刻。

@benjamingr “未定义状态”的想法是您无法知道它是否处于良好状态。 即使它在 99% 的情况下都很好,它仍然是“未定义的”,因为你不能确定。

@LinusU - 我明白你的意思。 尽管如此,为什么不立即处理连接错误并将错误存储在某些属性中(即this._connectionError )? 然后,您可以在.query方法中检查this._connectionError并将其传递给cb 。 恕我直言,这样做可以使代码更具可读性,并且更清楚地传达您要执行的操作。 在 Promise 可能已经被拒绝之后添加拒绝处理程序对我来说似乎很草率。 它将引发“unhandledRejection”事件,然后在调用.query后引发相应的“rejectionHandled”事件。 这很好,但似乎很奇怪。

我不想说“你不能那样编码”或“你的代码很愚蠢”。 我只是在谈论未处理的 Promise 拒绝的_默认行为_,如果您愿意,可以将其覆盖。 例如,如果我成功了,你可以覆盖我的默认行为,让你的代码像这样工作: process.on("unhandledRejection", function nop() {});

虽然同时,为了得到我想要的行为,我可以写:

process.on("unhandledRejection", function(reason, p) {
  if(process.listenerCount("unhandledRejection") === 1) {
    /* Throw some sort of uncaught exception here since there is no other
    unhandledRejection event handler */
    var err = new Error("Unhandled Promise rejection");
    err.reason = reason;
    err.promise = p;
    throw err;
  }
});

我不知道。 这似乎更像是一场宗教讨论,而不是其他任何事情。 开发人员已经使用 Promises 很长时间了,他们不希望看到他们的代码被破坏。 我明白了……但有时,破解代码是件好事?

全局覆盖的问题在于它适用于整个系统,包括您包含的所有模块。 这似乎很脆弱。

另外,这只是一个例子,我认为还有更多的情况需要一段时间来处理异常。 例如

async function cookMeal () {
  let fishPromise = getFish()
  let potatoPromise = getPotato()
  let saltPromise = getSalt()

  let salt = await saltPromise

  let fish = await combine(salt, await fishPromise)
  let potato = await combine(salt, await potatoPromise)

  return [fish, potato]
}

如果盐需要1000ms来获取,但在500ms之后获取鱼失败。 fishPromise直到await fishPromise $ 才会被处理。

@benjamingr “未定义状态”的想法是您无法知道它是否处于良好状态。 即使它在 99% 的情况下都很好,它仍然是“未定义的”,因为你不能确定。

未经处理的拒绝@LinusU也是这种情况。

全局覆盖的问题在于它适用于整个系统,包括您包含的所有模块。 这似乎很脆弱。

钩子的全部意义在于为发出未处理拒绝的 _all_ 承诺代码提供 _one_ 位置以进行处理。 这就是图书馆也实现这一点的原因。

另外,这只是一个例子,我认为还有更多的情况需要一段时间来处理异常。 例如

这实际上是反对throw默认值的一个非常鼓舞人心的例子。 我想知道是否应该允许异步函数在报告未处理的拒绝之前完成运行。 嗯……

@LinusU - 嗯,很好。 如果我们将 ES7 async/await 的东西拉到讨论中,我明白你的意思。 在您的示例中,我想您可以编写类似await Promise.all(saltPromise, fishPromise, potatoPromise)的内容。 但是,在某些情况下,您可能真的只想await salePromise并在等待其他承诺之前使用salt进行更多处理。

在这种情况下,我同意@benjamingr。 应该允许异步函数在未处理的拒绝甚至被报告之前完成。 那应该处理这种情况。

我相信使用 async/await 时,应该在调用 await 时抛出 throw,而不是在拒绝时。 这就是它在 c# 中的完成方式,它产生了非常直观的行为。 这也是我认为不应该发生未处理的拒绝日志记录的部分原因,因为一旦 async/await 登陆,就有一种更自然的方式来处理问题。

@Zoltu - 当然。 throw _does_ 发生在调用 await 时。 问题是,与此同时,从技术上讲,您有一个未经处理的拒绝。 建议的解决方案是静默忽略未处理的拒绝,直到异步函数终止并将控制权返回给事件循环。

不过,这个解决方案可能很棘手。 如果被拒绝的承诺具有全局范围并且可以被整个代码库访问(不太可能,但让我们继续吧)怎么办? 在这种情况下,Node 可能必须等待所有异步函数终止,然后再引发“unhandledRejection”事件。

TL;DR : async/await 解决了未处理的拒绝问题。 现在加入另一个解决方案(在等待 async/await 时)只会削弱未来的承诺,并且不会产生长期利益。


以 C# 为例,一旦 async/await 被开发人员直接停止使用Task s(与 ES Promise相同),低级库作者除外。 开发人员现在几乎完全通过_much_更直观的异步/等待模式语法与他们交互。 我怀疑一旦 async/await 普遍可用,ES 也会如此。

正因为如此,我认为在未来的某个时间点而不是现在考虑 Promise 的期望行为要重要得多。 目前,人们不得不直接与 Promise 进行交互,这就是为什么他们要为未处理的拒绝而苦苦挣扎。 将来,他们可能会通过 async/await 与他们交互,因此普通开发人员将不再需要关心未处理的拒绝(因为拒绝在await时间变成抛出)。

如果我们假设 ES async/await 的未来将类似于 C#,那么我认为可以肯定地假设直接与 Promises 交互的人将是高级库作者,而不是普通的 Web 前端 JavaScript 开发人员。 _against_ 记录未处理拒绝的参数往往来自高级使用场景(即,与将直接使用 Promise 的库作者相似的人口统计),而_for_ 记录未处理拒绝的参数往往来自更简单的使用场景(即,与未来的 async/await 用户类似)。

通过现在放入未处理的拒绝日志,我担心未来的承诺(异步等待后)将在没有任何真正长期利益的情况下削弱其能力(利益是短期的,在等待异步/等待着陆)。

很抱歉在这里翻牌,但我会再次改变主意。 让我们为未处理的拒绝设置默认行为,抛出未捕获的异常(出于上述相同的原因)

@LinusU - 我找到了一种重新编写示例的方法,以避免未处理的承诺拒绝。 见下文:

async function cookMeal () {
  let fishPromise = getFish();
  let potatoPromise = getPotato();
  let saltPromise = getSalt();

  let saltedFishPromise = Promise.all([saltPromise, fishPromise])
    .then(function(saltAndFish) {
      // Note: `saltAndFish` is an Array of 2 elements
      return combine(...saltAndFish);
    });
  let saltedPotatoPromise = Promise.all([saltPromise, potatoPromise])
    .then(function(saltAndPotato) {
      return combine(...saltAndPotato);
    });
  return await Promise.all([saltedFishPromise, saltedPotatoPromise]);
}

诚然,代码有点长,但它更接近地代表了我们正在尝试做的事情......即以最大的并发性做饭:) 上面的代码未经测试,所以请放轻松。

@bminer的论点从来都不是在没有未处理的拒绝触发的情况下编写代码是_不可能_。 争论是它_不明显_并且在实践中人们将编写确实报告未处理的拒绝的代码。

@benjamingr - 好吧,恕我直言,你可以选择写任何你想写的东西。 而且,如果您选择编写生成未处理拒绝的代码,则可以通过添加“unhandledRejection”事件处理程序来覆盖默认行为。 同样,如果您想编写抛出未捕获异常的代码,您可以添加一个“uncaughtException”事件处理程序。 除了异常是同步的,我看不出两者之间的区别。 而 Promise 拒绝是异步的。

想法?

编辑:我同意@LinusU编写的代码是我在意识到自己犯了一个错误之前可能会自己编写的东西......我(也许)在短时间内留下了一些未处理的承诺拒绝。 此外,我会争辩说(很可能),这个错误会在进程意外遇到未处理的拒绝之前逗留很长一段时间(默认行为(我认为)是进程终止)。

这归结为选择两个邪恶中的较小者:

  • 选项 A:我不关心未处理的拒绝,但我确实关心让我的流程保持活力。 我会让它们悄无声息地发生。
  • 选项 B:我绝对关心未处理的拒绝,尤其是在短时间内没有处理的情况下。 我宁愿捕捉错误也不愿让我的进程继续运行。

选项 B 似乎更符合未捕获异常的行为。 为什么要区别对待未处理的 Promise 拒绝? 因为编写代码以确保承诺拒绝永远不会未处理是_不明显_? 这真的有多重要? 仅在可能发生拒绝时才重要,对吗? 关于异常也可以说同样的论点。 假设你很懒,不想写try {} catch(e) {} 。 您的程序可以继续运行并且运行良好……直到实际发生异常。

顺便说一句,我认为这是一场重要的辩论,我仍然对这一切持观望态度。 想法?

还有几件事要在这里添加......

未处理的承诺拒绝不是一件好事。 据我了解,“unhandledRejection”事件的全部意义在于有机会处理承诺拒绝......或者(更有可能)报告错误。 因此,恕我直言,编写未处理 Promise 拒绝的代码并不是一个好习惯。 如果您这样做,您最终可能会报告不是真正的错误的错误......因为拒绝最终会得到“稍后”处理。 之后多少是未定义的,因此“unhandledRejection”事件处理程序并不真正知道该做什么。

另一点是可以覆盖默认行为(风险自负)。

默认情况下,编写代码的库编写者未处理承诺拒绝会受到惩罚; 他们的代码实际上是错误的,它可能导致进程崩溃。 我认为@LinusU提供的原始示例有一个错误——它允许承诺拒绝在不确定的时间内保持未处理; 因此,“unhandledRejection”事件处理程序不会真正知道该做什么。 将此报告为错误? 等待 30 秒,看看 promise 拒绝最终是否得到处理? 我们是否变得偏执并杀死了这个过程? 谁知道? 在那个宇宙中编写一个“unhandledRejection”事件处理程序......有点奇怪和尴尬。

如果我们只是默默地忽略默认的 promise 拒绝,我担心 Node 库会包含未处理 promise 拒绝的代码。 它(不可否认)更容易做到,所以这可能会发生。 如果您正在运行一个生产应用程序并希望将“unhandledRejections”报告为错误,那么您最终可能会从各处(来自 3rd 方库,甚至可能来自 Node 内部库)捕获错误,从而使“unhandledRejection”事件处理程序非常漂亮无济于事。

我们都写错误的代码; 没有人是完美的。 如果我们都开始使用 Promises,我们将如何捕捉这些错误? “unhandledRejection”事件处理程序是答案吗?

Fwiw 它仍然在我的盘子上尝试在 GC 上向console.error发出 unhandledRejections 警告。 (唯一想真正知道它是未处理的,真是一团糟。)需要一些工作,但我希望尽快解决。

GC会给你很多假阴性。 否则我们也会触发基于 GC 的事件。

@benjamingr这将如何给您带来误报?

GC会给你很多假阴性。 否则我们也会触发基于 GC 的事件。

我认为你错了。 基于 GC 的触发器不可能给出误报。

如果发生这种情况,您的系统中会出现错误并且您遇到了问题。

但是我们不能触发一个事件,或者即使我们触发了,它也是无用的,因为从那时起,promise 已经被 GC'd 并且我们没有合理的引用它。

(好吧,从技术上讲,v8 可能会复活它,但这几乎可以肯定是一个非常扭曲的想法。)

我不明白。

function foo(){
    var p = Promise.reject(Error());
    function f(){ return p; }
    function g() {}
    return g;
}
longLiving = foo();

v8 g 中的 IIRC 会泄漏 p,因为 v8 中的闭包是如何实现的。

绝对不能处理。 更不用说得到一个旧的console.error,如果一个promise使它进入旧堆。

澄清一下 - 你不会得到误报,但你会得到很多误报。

我认为你错了。 基于 GC 的触发器不可能给出误报。

如果发生这种情况,您的系统中会出现错误并且您遇到了问题。

该语句基于未处理的 Promise 拒绝是编程错误的断言进行操作。 对此的典型反例是预加载和即发即弃。 在第一种情况下,如果从未访问过预加载的资源,那么没有人关心它是否失败,因此没有人连接拒绝处理程序。 在第二种情况下,用户正在尝试尽力而为的操作,并且由于作者不在乎,因此故意不处理失败。

我相信在这个线程中进一步详细说明了为什么这些是可接受的编程范式以及为什么“仅将处理程序附加到所有内容”不是一个可行的响应,但如果需要,我不介意再次讨论它们。

澄清一下 - 你不会得到误报,但你会得到很多误报。

对不起,我读错了。

怎么样? GC 只会在没有任何东西可以引用 promise 并且没有任何东西可以影响它时才会发生。 如果那里有错误,则说明有错误。

哎呀,那时我们可能实际上应该抛出那个错误,但显然承诺是一个奇怪的地方,JS 的其余部分不适用,所以我们很久以前就同意警告是最好的主意。

@Zoltu - 我会断言未处理的承诺拒绝是编程错误......或者至少是糟糕的做法。 对于预加载,只需附加一个无所事事的拒绝处理程序。 这与编写一个空的 catch 块有什么不同? 作者可以选择不编写空的 catch 块并冒未捕获异常的风险,但这可能不是一个好主意。 同样的火和忘记。 为什么“只为所有事情附加一个处理程序”是不可接受的? 同样,这与同步代码中的 try/catch 有何不同?

预加载应该没有问题吧? 除非你预先加载东西然后扔掉它而不使用它......

class Foo {
    constructor() {
        this.authenticationPromise = authenticateWithCachedRefreshToken();
    }

    // user tries to do something that requires authentication
    onUserInteraction() {
        this.authenticationPromise.then(userProfile => {
            // TODO: use user profile to give results to the user
        }).catch(error => {
            // TODO: show the login dialog to the user with red error for failure
        });
    }

    // called when user submits login dialog
    onLogin(username, password, whatTheyWereTryingToDoBefore) {
        this.authenticationPromise = authenticateWithCredentials(username, password);
        whatTheyWereTryingToDoBefore();
    }
}

在此示例中,我们尝试使用本地存储中的刷新令牌对用户进行身份验证。 在某些情况下,我们会在用户交互发生之前完成(成功或失败)。 在其他情况下,我们不会,所以我们必须在解决/拒绝后执行用户交互。 一旦发生解决/拒绝,我们就可以处理解决或拒绝。 在此示例中,在拒绝的情况下,我们提示输入凭据,然后再次尝试使用凭据进行身份验证,重用authenticationPromise ,因此在等待凭据时发生的任何用户交互都将延迟到身份验证完成。 这使我们能够处理用户提交登录凭据然后在身份验证完成之前单击其他内容的情况。

未处理的拒绝日志记录的问题是,如果用户从不进行需要身份验证的交互,则此代码将打印错误! 我们投机取巧地尝试让用户登录,以便为他们提供良好的用户体验,但这可能会失败。 如果我们在authenticationPromise的结果上附加一个 catch 处理程序,那么拒绝就会被吞下,我们以后不能.catch()

人们过去提出的解决方法是:

constructor() {
    this.authenticationPromise = tryAuth();
    this.authenticationPromise.catch();
}

这样做的问题是,它会导致 _lot_ 的代码只是为了解决运行时在_认为_错误发生时(而不是在实际发生错误时)正在记录错误这一事实。

如果您正在使用很多 Promise 进行编程,这种模式变得非常普遍,并且必须将每一行代码变成 2 行很糟糕,尤其是当它们作为表达式出现时,现在需要包装在代码块中并返回声明补充说。 解决此问题的一种通用方法是立即捕获所有承诺来包装您的核心库,以使运行时的日志记录保持沉默,但这也是一个非常糟糕的解决方案。

正如我之前在这个线程中所说的,这整个问题都随着async/await消失了。 通过现在添加未处理的拒绝跟踪,我们所做的只是削弱承诺(使它们在某些情况下更难使用)来解决仅在我们等待async/await时存在的问题。

@Zoltu我认为您可能应该考虑阅读 _with_ async/await 做什么平台以及为什么(例如 C# 和 Python _extremely_ 自以为是。C# 甚至会在 _pending_ 异步操作(即使它没有被拒绝)默认情况下使请求崩溃) . 另外 - async/await 在这里实际上并没有解决任何问题。

如果我们忽略在构造函数中执行 i/o 是好还是坏的问题,我完全满足于人们必须明确地抑制未处理的应用程序和程序员错误。

C# 会将拒绝变成await上的异常,我认为这是正确的行为。 如果您直接与任务交互(没有await ),那么永远不会抛出异常,我也相信这是正确的行为。

当我之前对此进行研究时,只有飞镖(我相信)有未处理的拒绝行为。 每种带有承诺/未来/任务的语言都对未处理的拒绝/解决没有任何作用。

async/await 解决这个问题的原因是因为等待一个 promise 会为你把拒绝变成异常,所以你不能忘记捕捉。 如果您使用 await 来访问您的变量,您可以将代码包装在 try/catch 块中并假装它是同步的(大部分情况下)。 假设您正在使用 await 访问您的 Promise,您无需担心未处理的拒绝,因为它们会在正确的时间(在尝试访问时)变成异常。

我完全满足于人们必须明确地抑制未处理的应用程序和程序员错误。

我给出的例子不是程序员错误。 与在您知道如何处理拒绝的相同位置启动请求的更传统模型相比,该应用程序的行为与预期完全一致,并且具有更好的用户体验。

男孩,这肯定变成了一场宗教辩论,我觉得我迟到了。 :)

TL;DR -- 未处理的承诺拒绝很糟糕,MKAY!

@Zoltu - async/await 无助于解决问题。 await是添加拒绝处理程序的_explicit_ 方式,并且应该在_before_ 实际拒绝承诺之前到达此行(编辑:除非它被同步拒绝)。 如果你忘记写await或者你的程序意外地分支到await没有到达,你可能会有一个未处理的 Promise 拒绝。 因此,为了促进良好的编程实践,应该真正避免未处理的承诺——您应该在承诺有机会被拒绝之前为拒绝添加处理程序。 同样,您可能应该在承诺有机会成功之前添加承诺成功的处理程序......尽管这对于本次讨论来说并不重要。

我给出的例子不是程序员错误。 与在您知道如何处理拒绝的相同位置启动请求的更传统模型相比,该应用程序的行为与预期完全一致,并且具有更好的用户体验。

我首先要说有很多方法可以解决一个问题。 身份验证是一个有趣的问题。 我提出的一个问题是:如果登录凭据无效,您会拒绝 Promise,还是“成功”以null用户身份进行身份验证? 恕我直言,承诺拒绝应该很少见; 它们是同步异常的异步对应物。 否则很难争论,因为await将拒绝承诺变成了例外! 因此,恕我直言,只有在与数据库通信出现问题、连接断开等情况下,身份验证承诺才应拒绝; 它不应该仅仅因为用户的凭据错误而拒绝。 在同步领域,这是返回null与抛出异常之间的争论。 我的观点是,应该努力使承诺拒绝与异常一样罕见(出于性能原因和良好实践)。

如果在您的身份验证示例中承诺拒绝很少见,您可能会默默地忽略它们。 在某些情况下, .catch(nop)没有错。 同样,在某些情况下, catch(e) {}也没有错。

现在,我会备份一点。 我们可以整天抛出例子。 归根结底,无论是否使用 async/await,未处理的 Promise 拒绝都_不是_一件好事。 正如我之前提到的,这类似于不添加catch(e) {}块。 作为代码的作者,您当然可以这样做,但如果可能出现异常(即读取文件的内容),这可能不是一个好主意。 异步代码使这稍微复杂一些,因为未处理的拒绝_可能_会被处理......稍后的某个时间。 由于“稍后”是未定义的,因此最好假设在下一个事件循环滴答之后未处理的未处理拒绝会导致未捕获的异常。

我认为一般来说有一种更好的方式来编写代码,从而完全避免未处理的拒绝; 也就是说,在前面添加拒绝处理程序。

@bminer是的,您正在进入一个相当热门的话题。 :)

我认为从根本上说,目前有两种流行的承诺心理模型。 其中一个是拒绝与异常相同(如您所描述的),另一个更类似于打开的联合类型。

以下两个顺序代码块描述了两种心智模型(两者都没有异步工作):

try {
    let result = takeAction();
    processSuccess(result);
} catch (error) {
    processFailure(error);
}
let result = takeAction();
if (result.isSuccess) {
    processSuccess(result.success);
} else {
    processFailure(result.failure);
}

一旦异步进入画面,如果没有承诺,第一段代码就会变得难以编写,正如我们在所有浏览器 JS 历史中所看到的那样。 人们_想要_保持更简单的顺序心智模型,但以非阻塞方式工作,因此他们使用这样的 Promise:

takeAction().then(result => {
    processSuccess(result);
}).catch(error => {
    processError(error);
});

这是非常合理的,我完全承认以这种方式使用 Promise。 然而,我真正想要的不是上面的,而是我想要的:

try {
    let result = await takeAction();
    processSuccess(result);
} catch (error) {
    processFailure(error);
}

暂时退到一边,还有另一种使用模式正在寻找解决方案,那就是复杂的应用程序,它利用 Promise 不仅仅是将异步代码简化为顺序思维模型。 它全面利用承诺来处理任何事情都可能在任何时候发生的事实,并且多个事情可能希望共享某些异步任务的结果。 这种情况很难拟定,但总体思路是:

export class FooService {
    barPromise: Promise<Bar>

    constructor() {
        this.barPromise = getBar();
    }

    actionA() {
        this.barPromise.then(bar => {
            // work with bar in a way that is meaningful to actionA
        }).catch(error => {
            // handle errors getting bar in a way that is meaningful to actionA
        });
    }

    actionB() {
        this.barPromise.then(bar => {
            // work with bar in a way that is meaningful to actionB
        }).catch(error => {
            // handle errors getting bar in a way that is meaningful to actionB
        });
    }
}

在此示例中,有许多不同的操作(显示了两个)在 bar 可用之前无法完成。 他们想要共享一个 bar,但无法保证先发生哪个操作或获取 bar 是否会成功。 也有可能这两个动作都不会发生。 在这种情况下,随着应用程序复杂性的增加,越来越难以保证系统中的每个 Promise 都有附加的捕获。 当您开始跨越库边界时尤其如此,因为您无法对库内部的功能做出任何假设,因此通过库的_每个_promise 都需要在另一侧包装它。

为了完整起见,这就是上面使用 async/await 的样子:

export class FooService {
    barPromise: Promise<Bar>

    constructor() {
        this.barPromise = getBar();
    }

    actionA() {
        try {
            let bar = await barPromise;
            // work with bar in a way that is meaningful to actionA
        } catch (error) {
            // handle errors getting bar in a way that is meaningful to actionA
        }
    }

    actionB() {
        try {
            let bar = await barPromise;
            // work with bar in a way that is meaningful to actionB
        } catch (error) {
            // handle errors getting bar in a way that is meaningful to actionB
        }
    }
}

当 async/await 示例成为可能时,让我们快进到未来。 您现在有两个用例需要考虑。 一个是这样的用例,您的开发人员想要使用顺序思维模型,同时在后台利用非阻塞 IO。 在这种情况下,他们只使用 async/await 并且可能永远不必直接使用 Promise。 如果他们忘记了一个 catch 块,他们会在控制台中得到一个异常。 只要用户没有使用 Promise 进行任何高级操作,它们就会被完全覆盖并且可以生活在顺序世界中,尽管在后台进行了异步操作。

然而,在另一种情况下,用户正在更充分地利用 Promise。 他们使用它们就像使用 Java 中的Future或 C# 中的Task来在整个系统的多个使用场景中共享延迟结果,从而允许开发人员避免重复工作,同时仍然能够一旦他们准备好处理它,就以顺序的方式与 Promise 交互。

未经授权的拒绝是如何影响这一切的? 在顺序心智模型中,一旦 async/await 着陆,它们就不再需要,因为如果您使用 async/await,您将不再“忘记捕获”。 如果您没有使用 async/await,那么您_最有可能_以更高级的方式使用 Promise,在这种情况下,未经处理的拒绝可能是故意的。

直接使用 Promise 时仍有可能忘记一个问题,但我认为在 async/await 之外使用 Promise 的开发人员可能是更高级的开发人员,他们在做高级的事情,他们不搞砸的负担更高,这是一个允许这些高级用法的承诺的可接受成本。

TL;博士:

Promise 是几个不同事物的基石,其中之一是 async/await。 Promise 本身并不是一个完整的解决方案,我们不应该限制它们的能力,因为下一层(async/await)还没有完成。 通过将 Promise 考虑和设计为成功/失败联合,您可以让它们不仅仅用于 async/await 用例,而且它们仍然可以解决想要使用具有异常风格的顺序心智模型进行异步工作的问题安全网一旦 async/await 登陆。

在这一点上,我将建议将此转移到 PR 以作为众多提案之一。 这个问题已经变得过于广泛而无法产生效果,可能是时候让对特定提案最有热情的人发送 PR 并且人们可以在更加面向解决方案的环境中辩论其优点。

+1 用于关闭/锁定并注明欢迎 PR。

@mikeal如何为“什么都不做”提交 PR? ;)

不过说真的,公关方法_强烈_偏爱变革阵营而不是不变阵营。 另外,我相信已经有 PR 了? 虽然也许我在考虑 io.js ......

@mikeal - 很抱歉给大家发垃圾邮件,但我觉得这场辩论很重要。 很多开发人员仍在编写回调式异步代码,但由于 Promise 越来越受欢迎,为了未来的 Node 开发人员,需要在这里做出一个好的、明智的决定(即使他们不同意我的观点) . :) 回调听起来是个好主意,直​​到有一天我们“发现”了回调地狱……我们都在努力寻找一种更好的方法来开发软件。

无论如何,我同意 Github 问题跟踪器可能不是真正适合进行此类辩论的地方(PR 也可能不是),所以我不介意将讨论/辩论转移到其他地方。 有这类论坛吗?

@Zoltu - 感谢您的回复。 据我所知,您的论点是未处理的拒绝可能无关紧要(在使用 async/await 的情况下,因为您可能稍后会处理它们)或故意的(因为您在更高级的环境中直接使用 Promises大大地)。 那么为什么首先要有一个“unhandledRejection”处理程序呢?

Promises 中的简单语法错误怎么办?

var promise = new Promise(function(resolve) {
    kjjdjf();  // function that doesn't exist because I failed at typing
});
// This results in an unhandled promise rejection, not an exception

像这样的错误最终会如何被抓住? 你是说我在使用没有 async/await 的 Promises,所以我们仍然应该丢弃错误吗?

@bminer这里有一个关于 ES6 的更彻底的讨论: https ://github.com/promises-aplus/unhandled-rejections-spec/issues

在您的示例中,如果您使用的是 async/await,那么一旦您尝试访问承诺的结果,就会得到一个异常:

var promise = new Promise(function(resolve) { kjjdjf(); });
// some other stuff
await promise; // throws

如果你没有使用 async/await,你最终会得到这样的结果:

var promise = new Promise(function(resolve) { kjjdjf(); });
// some other stuff
promise.then(() => {
    // never called
}); // no throw!

第二个例子是每个人都在尝试通过非人工拒绝跟踪来解决的问题,我承认尝试解决这是一个合理的问题,我只是认为今天不值得用一个糟糕的解决方案来解决它明天会有更好的解决方案(等待)。

需要明确的是,我反对任何未处理的拒绝处理程序,尽管特别是默认的未处理拒绝处理程序具有明显的副作用,例如使应用程序崩溃或登录到控制台。 Chrome 目前会自动将所有未处理的 Promise 拒绝记录到控制台,因此我的应用程序的控制台中充斥着拒绝处理程序,因为我对处理程序进行了后期绑定(如之前的 auth 示例)。

抛出错误是我喜欢的最明智的默认选项。

我在 JS 中编写了很多实用程序脚本,我对此感到非常无聊

process.on("unhandledRejection", (reason, promise) => {
  throw reason;
});

_every_ 独立 JS 文件中的口头禅或等效导入。 有些脚本可以只写同步,但只要你得到httpsql没有同步等效的地方,它就来了。

而且,老实说,提供的记录无所作为的论据对我来说听起来像是一个笑话。
唯一有效的反驳是该死的向后兼容性。

@ivan-kleshnin 什么时候会抛出错误,堆栈跟踪会是什么样子?

@Zoltu不确定我是否正确。 你能详细说明吗?
我可以在堆栈跟踪中再添加一项。 如果它是故意的,即明确的,我也可以隐藏未处理的错误。 默认情况下隐藏错误闻起来像 PHP,破坏了我对平台的信任。
特别是,在“错误”中,我们包括“语法错误”,如@bminer已经说明的。

@Zoltu这根本不是 C# 所做的。 C# 有一个执行上下文的概念——例如 ASP.NET 将_使请求失败_即使没有_没有未处理的拒绝_只是因为在请求上下文中创建了异步操作并且在您发送响应时没有解决.

除此之外:

我在这里 100% 支持@mikeal ,我有另一个建议:我们等待 TC 完成官方的承诺挂钩,等待看看浏览器实现了什么,并且_在一年内_我们会采用 _从那时起_浏览器已经解决的任何行为将是_事实上的标准_。

等着看浏览器实现了什么,一年后我们会采用浏览器已经确定的任何行为,因为这将成为事实上的标准。

@benjamingr - 我不介意这个想法,但 Node 也可以设定标准。 :) 另外,浏览器不会因未捕获的异常而“崩溃”; 他们有一些愚蠢的onerror处理程序并将错误转储到控制台。 在这方面,Node 只是一个不同的野兽。 想法?

所有人都赞成将此讨论移至unhandled-rejections-spec 问题页面,说“是”。

@bminar那个站点和规范已经死了,也许@domenic想恢复它们,但老实说,我认为他正忙着做一百万其他事情并让浏览器首先添加钩子。

此外 - node _can_ 设置标准,但我认为如果用户习惯于浏览器登录到控制台,那么默认情况下 node 中的类似行为可能是一个好主意。

@paulirish当您调查承诺选项卡时,你们是否获得了有关仪器的任何有意义的信息? 导致选择“将未处理的拒绝记录到控制台”的过程是否记录在某处?

同样 - 谁在其他浏览器中实现了这一点?

刚刚遇到这个问题,它让我想起一个月前我在ES邮件列表中提出了一个新的基于延续的解决方案,而不是promise ,每个人都试图说服我promise是正确的方法,经过一些搜索我也发现这个组合功能提案

现在我建议大家仔细看看这个提案和我的实现,IMO 解决了大部分的承诺问题,因为 async-await 提案将进入 ES7,并且选择异步原语的大门正在关闭,请付费一些关注和感谢!

@winterland1989您从cb(err, data)cb(dataOrErr)的更改使其立即与其他所有开箱即用的节点模块不兼容。 我什至更喜欢rejectresolve比这更好。 为什么选择只做一个参数然后引入instanceof Error检查?

@LinusU感谢您阅读我的实现!

选择cb(dataOrErr)接口的原因是为了简化错误处理,而这种设计选择使retry/race/parallel...的实现更加简洁。 我承认这在某种程度上是一种任意的设计选择。

当然这只是compositional-functions proposal的一个实现细节,如果实在惹恼了很多人,我们可以对最终实现的接口展开讨论。

底线是,由于 javascript 在某种程度上是一种函数式语言,我们实际上不必使用基于状态机的解决方案(promise)来锁定自己,它具有明显的缺陷,例如错误处理和取消。 从其他语言借用一些功能性的想法,例如延续和 call-cc 并没有什么坏处。

我将结束此讨论,因为我们无法在此讨论或任何提案方面取得进展。

欢迎 PR 和进一步讨论。

@Zoltu

在这种情况下,他们只使用 async/await 并且可能永远不必直接使用 Promise。 如果他们忘记了一个 catch 块,他们会在控制台中得到一个异常。

如果我错了或者我说得不对,请纠正我,但那是不正确的,不是吗? 使用await需要一个封闭的async函数(我注意到您的示例中缺少此功能),因此如果您忘记使用try/catch它只会成为拒绝值保证 _that_ 函数返回,然后你回到你开始的地方 - 你必须在 _somewhere_ 抓住它,否则它会被吞下,因此async/await有完全相同的问题。

我之所以提出这个问题,是因为作为 Node 的新手,我最近在通过 Babel 使用async/await时遇到了这个问题。 如果我对即将发生的事情有任何迹象,将会有人盯着他们的日志输出半小时想知道为什么他们的传入服务器请求会静默失败并在浏览器中超时。 事实上,我的函数中确实有一个try/catch块,但是错误发生在一个(绝对是同步的) JSON.stringify调用中,我无知地离开了它(吸取了教训)。 如果默认情况下记录了未处理的拒绝(就像在 Chrome 中一样),我大概会看到“未处理”这个词以及错误并立即知道我做错了什么。 就目前而言,我将在我构建的每个 Node 应用程序中添加process.on('unhandledRejection', err => { console.error('Unhandled promise rejection ', err.stack); });或类似的东西,因为如果我或其他任何人使用我的代码再次出现问题,它将提供多少信息。

@noinkling嗯,在 C# 中,您不允许在未标记为async的函数中使用await #$ 。 我没有意识到 ES 允许您在其中使用await的非异步函数。 我以前从未真正考虑过,但我想知道这种问题是否是 C# 强制您将方法用await标记为async的原因?

@Zoltu您误解了我,我是说ES _does_ 当前需要一个封闭的async函数才能使用await (尽管据我了解,顶级await正在考虑中)。 您在给出的示例中将其遗漏了。 因此,由于任何await都需要一个封闭的async函数(它返回一个 Promise),因此默认情况下永远不会记录当前 Node 行为错误,它们将像任何其他 Promise 一样被吞下,除非显式处理,即使它们被临时“转换”为函数内的异常。

换句话说, async/await并没有解决任何与日志记录问题有关的问题,这是有道理的,因为它基于底层的 Promise。

啊,你是对的,我误解了你,我认为你是对的,async/await 只有在你到处使用它时才有帮助。 如果您使用await专门与 Promise 进行交互,那么您将不会遇到任何麻烦,并且不需要未处理的拒绝处理程序。 如果你有时直接与 Promise 交互(例如, .then(...) ),那么你必须更加小心。

根据我使用 C# 的经验,很少有人直接与Task s(promise 等价物)进行交互。 他们都通过await关键字与他们互动。 唯一直接与Task交互的人是需要更高级控制的人,而这组人将受益于没有未处理的拒绝处理程序。

如果您使用await专门与 Promise 进行交互,那么您将不会遇到任何麻烦,并且不需要未处理的拒绝处理程序。 如果你有时直接与 Promise 交互(例如, .then(...) ),那么你必须更加小心。

虽然没有区别(除了语法) - 你使用.catch和承诺或try/catchawait ,否则错误是沉默的。 无论哪种方式,您都必须明确处理它,否则不会记录任何内容。 重申一下: async/await与 Promise 相比,在日志记录行为方面没有任何改变,因此不能解决静默错误的问题。 在 C# 中可能会有所不同,我不知道。

也许我误解了一些东西,但据我所知,如果你_总是_通过await与承诺进行交互,那么你会错过异常的唯一方法是你从未查看过承诺的结果。

let fooPromise = getPromise(); // no throw here, we haven't tried to interact with the result yet
let barPromise = getPromise(); // will never throw, but since you never awaited the result it seems you don't really care
let foo = await fooPromise; // exception thrown here
actOnFoo(foo); // we'll either get here or we'll see an exception bubble up the stack
let fooPromise = getPromise();
let barPromise = getPromise();
fooPromise.then(result => {
    let foo = result;
    actOnFoo(foo); // we may never get here *AND* never see an exception, which is one reason why `await` is better
});

这种做法依赖于在调用堆栈中一直使用 async/await。 也许您所指的问题是在最顶层(如您所提到的)您必须调用您的顶级函数。 我同意,Node 应该允许使用顶级函数中的await 。 与此同时,我会推荐这样的东西:

async main() {
    // TODO: write application in here, use `await` everywhere interacting with promises
}

main().catch(error => console.log(error));

话虽如此,我们现在正在讨论风格问题,这种风格取决于不在全局空间中执行代码(这在 ES 中似乎很常见)。 C# 有同样的问题,你可以在这里看到同样的答案: http://stackoverflow.com/a/24601591/879046。 然而,由于 C# 只有一个入口点,它是一个函数,因此不可能在包装的执行上下文之外意外执行代码,因此它比在 Node.js 中更难搞砸。

让我做一个更公平的比较:

async function doAsyncStuff() {
  await promiseThatRejects();
}
doAsyncStuff().catch(error => console.log(error));

对比

function doAsyncStuff() {
  return promiseThatRejects();
}
doAsyncStuff().catch(error => console.log(error));

任何可以使用await来传播错误的地方,都可以使用返回 Promise 来达到相同的效果(这通常是您使用 Promise 来维护扁平结构的方式)。 当然,在这种情况下,第二个示例中的包装函数是不必要的,但需要进行公平比较,并且在实际代码中将取决于上下文(由于与其他 API 交互或其他原因)。 这可能更现实:

async function doAsyncStuff() {
  try {
    await getPromise();
    // ...
  } catch (error) {
    console.log(error);
  }
}
doAsyncStuff();

对比

getPromise()
  .then(...)
  .catch(error => console.log(error));

但这与手头的问题无关。

承诺和async/await首先存在的主要原因之一是避免深度嵌套的调用堆栈,因此首先不应该过多关注堆栈上的错误传播......但是如果由于某种原因它_is_,那么async/await没有提供标准承诺在这方面无法提供的任何东西,而且事实仍然是,如果错误被明确捕获并记录在某处,这两种范式都不会进行任何日志记录(是的,顶级计数)。

_If_ 和 _when_顶级await使其成为语言,那么我认为您可能有一点,这取决于如何处理该级别的错误。 这是一个很大的“如果”和一个很大的“何时”。

如果您仍然不同意,请随时 PM 我,我并不是要通过这个讨论来劫持线程。

我不会说话,“PromiseRejectionHandledWarning”默认是废话DX。 Promise 的大部分意义在于您可以异步处理 Promise 拒绝,现在一堆完全有效的 Promise 模式会打印垃圾邮件警告,除非您调整默认值。

before(function() {
  sinon.stub(obj.asyncFunction).returns(Promise.reject(err));
});

it('my test', function (done) {
  obj.asyncFunction.catch(error => {
    assert.ok(error);
    done();
  });
});
let lastPromise;

stream.on('data', () => {
  if (lastPromise) {
    lastPromise = lastPromise.then(() => doSomethingAsync(), () => doSomethingAsync());
  } else {
    lastPromise = doSomethingAsync();
  }
});

等等。这个警告​​对调试非常有用,但你需要记住在 prod 中关闭它,如果你正在做任何事情,甚至是远程不平凡的承诺。

但是您需要记住在 prod 中将其关闭,并且如果您正在做任何事情,即使是对 promise 来说也不是微不足道的事情。

这将是正确的,但作为默认设置,它可以被那些可能无法很好地解决潜在问题的人使用。

不一定,只是让完全有效的模式看起来比实际更可怕,并使执行完全合理的事情的 npm 模块在上游打印警告,从而使最终用户不得不担心模块行为。 修复承诺地狱不是节点的工作,这就是 async/await 或 co/yield 的用途。 然后,当人们开始写关于异步/等待地狱的博客时,您可以想出另一个半生不熟的“儿童轮模式”黑客,例如用于回调的域或将警告记录到控制台以获取承诺:)

我仍然认为未处理拒绝的默认(但可自定义)行为是使应用程序崩溃,就像未捕获的异常一样。

如果您想在 Promise 拒绝发生后对其进行处理,您可以绑定自己的“unhandledRejection”处理程序来执行此操作。 但是,我们不能默认忽略未处理的 Promise 拒绝。 您的自定义“unhandledRejection”处理程序可以执行诸如启动 30 秒计时器之类的操作,以查看 Promise 是否最终得到处理。 但是,最终,所有的 Promise 拒绝都应该被处理......如果没有,它可能是一个错误,甚至可能是一个应该故意使应用程序崩溃的错误。

恕我直言,未处理的拒绝可能与未捕获的异常一样危险。 应用程序可能处于未定义状态。 例如,即使是 SyntaxErrors 也可能导致未处理的拒绝。

那么每个节点模块都应该有自己的on('unhandledRejection')特殊雪花处理程序吗? 如果某个模块希望行为“使应用程序崩溃”而另一个模块希望它“忽略它”怎么办? 全球行为是全球性的,儿童图书馆无法在没有冲突风险的情况下改变它。 这种功能的重点是让最终用户应用程序决定它想要如何处理未处理的拒绝,而不是让节点决定应该允许节点模块使用哪些承诺模式,这就是它现在所做的。

@vkarpov15 - 回答你的问题:是的。 恕我直言,后期绑定拒绝处理程序是不好的做法——库应该避免这种模式。 如果他们真的想这样做,他们可以拥有自己的“unhandledRejection”处理程序。

也许 Promises 应该有一个标志来指示在构建时需要后期绑定? 另外,单独讨论,但也许 Promises 应该有一个.cancel()方法来中止异步操作?

这远非不好的做法,它是更高级用例的必要做法,这就是模块应该提供的包装器。 如果我错了,请纠正我,但“unhandledRejection”处理程序对整个进程来说是全局的,这意味着想要在未处理的拒绝上崩溃的模块将搞砸所有其他模块。 TBH 我理解你的意思,如果你在 2015 年初问我,我会提出类似的论点,“不好的做法不要这样做”。 但是越来越习惯于 Promise 并花时间在 observables 领域让我意识到将 Promise 限制为“让我将一堆 HTTP / 数据库调用链接在一起”实际上是对 Promise 可以做什么的狭隘观点,这就是这个默认值限制你。

这个讨论已经进行了好几次,共识是现在警告它,当我们有钩子的时候抛出 GC。

很容易附​​加一个空的 catch 处理程序来抑制警告。

至少有 5 个这样的线程 - 如果您想要更多最新的参数,我建议您查看 NodeJS/promises 之一。

在此处引用相关 PR:#8217
从节点 7 开始将出现进程警告

我编写了caught模块来简化@MicahZoltu@vkarpov15描述的用例的工作,而无需通过监听事件来全局更改所有内容的行为。

它最初是作为 Stack Overflow 上这个答案的示例编写的:我应该避免异步处理 Promise 拒绝吗? 但也许它可能对阅读此讨论的人有用。

此页面是否有帮助?
0 / 5 - 0 等级