何时使用.then(success, fail)被认为是Promise中的反模式?

234

我查看了 bluebird 的 Promise FAQ,其中提到 .then(success, fail) 是一种反模式。但我不太明白它对于 trycatch 的解释。

some_promise_call()
.then(function(res) { logger.log(res) }, function(err) { logger.log(err) })

看起来示例表明以下方式是正确的。

some_promise_call()
.then(function(res) { logger.log(res) })
.catch(function(err) { logger.log(err) })

有什么区别吗?


3
then().catch() 更易读,因为您不需要查找逗号并确定回调函数是成功还是失败分支。 - Krzysztof Safjanowski
15
受到“看起来更好”的争议,KrzysztofSafjanowski感到沮丧。完全错误! - Andrey Popov
1
@AndreyPopov,你认为哪个更好看?请阅读下一个答案,哪个更易读:.then(function(res) { logger.log(res) }, function(err) { logger.log(err) }) 或者 .then(function(res) { logger.log(res) }).catch( function(err) { logger.log(err) }) - Krzysztof Safjanowski
11
注意:当使用.catch时,您不知道哪一步引起了问题 - 是在最后一个then内部还是在承诺链的其他地方。因此,它确实具有自己的缺点。 - vitaly-t
2
我总是在Promise的.then()参数中添加函数名称以使其易读,例如:some_promise_call() .then(function fulfilled(res) { logger.log(res) }, function rejected(err) { logger.log(err) }) - Shane Rowatt
7个回答

260

有什么区别吗?

.then()调用将返回一个 Promise,如果回调函数引发错误,则该 Promise 将被拒绝。这意味着当成功的logger失败时,错误会传递给以下的 .catch() 回调,而不是与success一起使用的fail回调。

下面是一个控制流程图:

带有两个参数的 then 的控制流程图 then catch 链的控制流程图

同步代码表达如下:

// some_promise_call().then(logger.log, logger.log)
then: {
    try {
        var results = some_call();
    } catch(e) {
        logger.log(e);
        break then;
    } // else
        logger.log(results);
}

第二个log(类似于传递给.then()的第一个参数)只会在没有异常发生的情况下执行。标记块和break语句感觉有点奇怪,这实际上是Python使用try-except-else来解决这个问题的原因(推荐阅读!)。

// some_promise_call().then(logger.log).catch(logger.log)
try {
    var results = some_call();
    logger.log(results);
} catch(e) {
    logger.log(e);
}

catch日志记录器还将处理成功日志调用引发的异常。

至此,就差不多了。

我不太理解 try 和 catch 的解释

有人认为通常情况下,您希望在处理过程的每个步骤中捕获错误,并且不应在链式操作中使用它。期望只有一个最终处理程序来处理所有错误。而当您使用“反模式”时,则无法处理某些 then-callback 中的错误。

然而,这种模式实际上非常有用:当您希望处理发生在当前步骤中的错误,并且当没有错误发生时要执行完全不同的操作时,即当错误是不可恢复的时。请注意,这会分支您的控制流程。当然,有时这是想要的。


以下代码有什么问题?

some_promise_call()
.then(function(res) { logger.log(res) }, function(err) { logger.log(err) })
你需要重复回调。你更想要什么?
some_promise_call()
   .catch(function(e) {
       return e; // it's OK, we'll just log it
   })
   .done(function(res) {
       logger.log(res);
   });

你也可以考虑使用.finally()来实现这个功能。


12
这是我几天来读过的最有帮助的解释(我读了很多)。我无法表达我有多感激! :) 我认为你应该更强调两者之间的区别,即.catch可以捕捉成功函数中的错误。个人而言,我觉得这是极其错误的,因为你最终只有一个错误入口点,可能会有多个操作引起多个错误,但这是我的问题。不管怎样 - 感谢你的信息!你有没有一些在线沟通工具想分享,这样我就可以询问更多问题? :P - Andrey Popov
@AndreyPopov 如果你想的话,可以来聊天室 - Bergi
2
希望这个链接能让你在这里获得更多的赞。绝对是这个网站上关于重要的“Promise”机制最好的解释之一。 - Patrick Roberts
2
.done() 不是标准的一部分,对吧?至少 MDN 没有列出这个方法。如果有的话会很有帮助。 - ygoe
1
@ygoe 确实。done 是 Bluebird 的一个东西,基本上已经被 then+未处理拒绝检测所取代。 - Bergi
3
来自一位色盲的留言:这些图示一点也不清晰 :) - Benny K

44
两者并不完全相同。区别在于第一个例子不能捕获在success处理程序中抛出的异常。因此,如果您的方法只应返回已解决的承诺,这通常是必须的,您需要一个尾随的catch处理程序(或另一个带有空success参数的then)。当然,可能是您的then处理程序不会执行任何可能失败的操作,在这种情况下,使用一个带有两个参数的then可能是可以的。
但我认为您所链接的文章的重点在于then与回调相比,在链式连接多个异步步骤方面非常有用,而当您真正这样做时,then 的两个参数形式不太符合预期,上述原因就是一个很微妙的问题。这在链式中间使用时特别令人费解。
作为一个做过很多复杂异步工作并且遇到过比我愿意承认的角落情况的人,我真的建议避免这个反模式,转而采用分离的处理程序方法。

20

通过比较这两种方法的优缺点,我们可以得出一个合理的猜测,看哪一种适合当前情况。这是实现承诺的两种主要方法,它们都有利有弊。

Catch方法

some_promise_call()
.then(function(res) { logger.log(res) })
.catch(function(err) { logger.log(err) })

优点

  1. 所有错误都在一个catch块中处理。
  2. 即使在then块中捕获任何异常。
  3. 可以链接多个成功的回调函数。

缺点

  1. 在链接多个回调函数时,难以显示不同的错误消息。

成功/错误方法

some_promise_call()
.then(function success(res) { logger.log(res) },
      function error(err) { logger.log(err) })

优点

  1. 您可以获得细粒度的错误控制。
  2. 您可以为各种错误类型(如数据库错误、500错误等)设置通用错误处理函数。

缺点

  1. 如果您希望处理成功回调抛出的错误,仍然需要另一个catch

对于需要仅使用日志文件调试生产问题的人,我更喜欢采用成功/错误方法,因为它可以提供创建因果错误链的能力,该链可以在应用程序的退出边界处记录。 - Shane Rowatt
问题。假设我进行了一个异步调用,它会执行以下几个操作之一:1)成功返回(2xx状态码),2)未能成功返回(4xx或5xx代码),但不是被拒绝的,3)或根本没有返回(因为网络连接中断)。对于情况#1,在.then中的成功回调被触发。对于情况#2,在.then中的错误回调被触发。对于情况#3,.catch被调用。这是正确的分析,对吗?情况#2最棘手,因为技术上4xx或5xx不是拒绝,它仍然成功返回。因此,我们需要在.then内处理它。...我的理解正确吗? - Benjamin Hoffman
对于第二种情况,在.then中的错误回调被触发。对于第三种情况,.catch被调用。这是正确的分析,对吗?- 这就是fetch的工作方式。 - aWebDeveloper

2

简单解释:

在ES2018中

当使用onRejected参数调用catch方法时,会执行以下步骤:

  1. 将promise设置为this值。
  2. 返回? Invoke(promise, "then", « undefined, onRejected »)。

这意味着:

promise.then(f1).catch(f2)

equals

promise.then(f1).then(undefiend, f2)

1
使用.then().catch()可以启用Promise Chaining,这是实现工作流所必需的。您可能需要从数据库中读取一些信息,然后将其传递给异步API,然后您可能希望操作响应。您可能希望将响应推回到数据库中。使用您的概念处理所有这些工作流是可行的,但非常难以管理。更好的解决方案是then().then().then().then().catch(),它在一个catch中接收所有错误,并让您保持代码的可维护性

1
使用then()catch()有助于在promise上链接成功和失败处理程序。catch()处理由then()返回的promise。它处理以下内容:
  1. 如果promise被拒绝。请参见图片中的#3
  2. 如果在以下行号4到7之间的then()成功处理程序中发生错误。请参见图片中的#2.a (then()的失败回调无法处理此错误。)
  3. 如果在then()的失败处理程序中的第8行发生错误。请参见图片中的#3.b。
1. let promiseRef: Promise = this. aTimetakingTask (false); 2. promiseRef 3. .then( 4. (result) => { 5. /* 成功地解决了promise。 6. 在这里处理数据 */ 7. }, 8. (error) => console.log(error) 9. ) 10. .catch((e) => { 11. /* 成功地解决了promise。 12. 在这里处理数据 */ 13. });

enter image description here

注意:如果已经写了catch(),很多时候失败处理程序可能没有被定义。 编辑:reject()只有在then()中的错误处理程序被定义时才会调用catch()。请注意图中第3号提示的catch()。当第8行和第9行的处理程序未被定义时,它将被调用。 这是有道理的,因为由then()返回的Promise如果回调函数负责处理它,则不会出现错误。

从数字3到“catch”回调的箭头似乎是错误的。 - Bergi
谢谢!在then()中定义了一个错误回调,它不会被调用(代码片段中的第8和9行)。#3调用其中的两个箭头之一。这是有道理的,因为如果回调函数负责处理错误,则由then()返回的Promise不会有错误。回答已经修改! - Venkey

-2

不要说话,给个好例子。以下代码(如果第一个 Promise 已解决):

Promise.resolve()
.then
(
  () => { throw new Error('Error occurs'); },
  err => console.log('This error is caught:', err)
);

等同于:

Promise.resolve()
.catch
(
  err => console.log('This error is caught:', err)
)
.then
(
  () => { throw new Error('Error occurs'); }
)

但是由于第一个 Promise 被拒绝,这并不相同:

Promise.reject()
.then
(
  () => { throw new Error('Error occurs'); },
  err => console.log('This error is caught:', err)
);

Promise.reject()
.catch
(
  err => console.log('This error is caught:', err)
)
.then
(
  () => { throw new Error('Error occurs'); }
)

4
这个回答不合逻辑,你能否请删除它?它会引起误导,并使正确答案被忽视。 - Andy Ray
@AndyRay,这在实际应用中没有意义,但理解承诺的工作是有意义的。 - ktretyak
我认为这段代码需要一些注释,以便我们理解它想要表达的意思。它们如何相同,又如何不同? - Ruan Mendes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接