如何正确处理 Promise 链中的错误?

Posted

技术标签:

【中文标题】如何正确处理 Promise 链中的错误?【英文标题】:How to handle error properly in Promise chain? 【发布时间】:2017-08-21 22:55:44 【问题描述】:

假设我们有 3 个返回 Promise 的异步任务:ABC。我们希望将它们链接在一起(即,为了清楚起见,获取A 返回的值并用它调用B,但也希望正确处理每个错误,并在第一次失败时爆发。目前,我看到了两种方法:

A
.then(passA)
.then(B)
.then(passB)
.then(C)
.then(passC)
.catch(failAll)

这里,passX 函数处理对X 调用成功的每个。但在failAll 函数中,我们必须处理ABC所有错误,这可能很复杂且不易阅读,尤其是如果我们有超过 3 个异步任务。所以另一种方式考虑到这一点:

A
.then(passA, failA)
.then(B)
.then(passB, failB)
.then(C)
.then(passC, failC)
.catch(failAll)

在这里,我们将原始failAll 的逻辑分离为failAfailBfailC,这看起来简单易读,因为所有错误都在其源旁边处理。 但是,这并没有达到我想要的效果。

让我们看看A是否失败(拒绝),failA不能继续调用B,因此必须抛出异常或调用reject。但是这两个都被failBfailC 捕获,这意味着failBfailC 需要知道我们是否已经失败了,大​​概是通过保持状态(即变量)。

此外,似乎我们拥有的异步任务越多,failAll 函数的大小就会增加(方式 1),或者调用更多的 failX 函数(方式 2)。这让我想到了我的问题:

有更好的方法吗?

考虑:既然then中的异常是由reject方法处理的,那么是否应该有Promise.throw方法来真正的断链呢?

possible duplicate,答案是在处理程序内添加更多范围。承诺不应该尊重函数的线性链接,而不是传递函数传递函数的函数吗?

【问题讨论】:

Break promise chain and call a function based on the step in the chain where it is broken (rejected)的可能重复 @wing 抱歉,我也应该澄清一下——在函数passXfailX 中,我实际上是在“处理对X 的调用的成功”。例如,A 是对 API 的 XMLHTTPRequest GET 调用,passA 将相应 DOM 对象的状态更新为“已获取”,然后继续调用另一个 API B。我会在我的问题中说明这一点。 承诺不应该尊重函数的线性链接” - 不适用于比链接更多的错误处理。来自Proper way to skip a then function in promises:对于分支,您将始终需要额外的嵌套级别。 承诺的意义在于您仍然可以return 摆脱这些嵌套函数,而无需调用回调。 【参考方案1】:

我推荐了两种方法(取决于你想用这个来完成什么):

是的,您希望通过一次 catch 处理承诺链中的所有错误。

如果你想知道哪一个失败了,你可以用一个独特的消息或值来拒绝承诺,如下所示:

A
.then(a => 
  if(!pass) return Promise.reject('A failed');
  ...
)
.then(b => 
  if(!pass) return Promise.reject('B failed');
  ...
)
.catch(err => 
  // handle the error
);

或者,您可以在 .then 中返回其他承诺

A
.then(a => 
  return B; // B is a different promise
)
.then(b => 
  return C; // C is another promise
)
.then(c => 
  // all promises were resolved
  console.log("Success!") 
)
.catch(err => 
  // handle the error
  handleError(err)
);

在每个承诺中,您都需要某种独特的错误消息,以便知道哪个失败了。

由于这些是箭头函数,我们可以去掉大括号!这只是我喜欢承诺的另一个原因

A
.then(a => B)
.then(b => C)
.then(c => console.log("Success!"))
.catch(err => handleError(err));

【讨论】:

【参考方案2】:

您有几个选择。首先,让我们看看我是否可以提炼出您的要求。

    您希望在错误发生的位置附近处理错误,这样您就没有一个错误处理程序必须对所有可能的不同错误进行排序以查看如何处理。

    当一个 Promise 失败时,您希望能够中止链的其余部分。

一种可能是这样的:

A().then(passA).catch(failA).then(val => 
    return B(val).then(passB).catch(failB);
).then(val => 
    return C(val).then(passC).catch(failC);
).then(finalVal => 
    // chain done successfully here
).catch(err => 
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
);

然后,在每个failAfailBfailC 中,您都会收到该步骤的特定错误。如果要中止链,请在函数返回之前重新抛出。如果您希望链继续,您只需返回一个正常值。


上面的代码也可以这样写(如果passBpassC抛出或返回一个被拒绝的promise,行为会略有不同。

A().then(passA, failA).then(val => 
    return B(val).then(passB, failB);
).then(val => 
    return C(val).then(passC, failC);
).then(finalVal => 
    // chain done successfully here
).catch(err => 
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
);

由于这些是完全重复的,因此您也可以使整个事情成为任何长度的序列的表驱动。

function runSequence(data) 
    return data.reduce((p, item) => 
        return p.then(item[0]).then(item[1]).catch(item[2]);
    , Promise.resolve());


let fns = [
    [A, passA, failA],
    [B, passB, failB],
    [C, passC, failC]
];

runSequence(fns).then(finalVal => 
    // whole sequence finished
).catch(err => 
    // sequence aborted with an error
);

链接大量 Promise 时的另一个有用点是,如果您为每个拒绝错误创建一个唯一的 Error 类,那么您可以更轻松地在最终的 instanceof 处理程序中使用 instanceof 来切换错误类型(如果需要)知道哪个步骤导致了中止的链。像 Bluebird 这样的库提供了特定的 .catch() 语义,用于生成仅捕获特定类型错误的 .catch()(就像 try/catch 的方式一样)。你可以在这里看到 Bluebird 是如何做到的:http://bluebirdjs.com/docs/api/catch.html。如果您要在每个错误自己的承诺拒绝时处理每个错误(如上面的示例),那么这不是必需的,除非您仍然需要在最后的.catch() 步骤中知道哪个步骤导致了错误。

【讨论】:

【参考方案3】:

您可以branch the promise-chain,但老实说,早期错误处理并不是正确的方法,尤其是出于可读性等模糊原因。同步代码也是如此,即不要 try/catch 每个函数,否则可读性会被扔进垃圾箱。

始终传递错误并在正代码流恢复时“处理”它们。

如果你想知道事情进展到什么程度,我使用的一个技巧是一个简单的进度计数器:

let progress = "";
A()
.then(a => (progress = "A passed", passA(a)))
.then(B)
.then(b => (progress = "B passed", passB(b)))
.then(C)
.then(c => (progress = "C passed", passC(c)))
.catch(err => (console.log(progress), failAll(err)))

【讨论】:

但是当A 失败时,failB 将被调用null。我认为这正是他不想要的。 @Bergi 你是对的。我已经改变了答案。谢谢!

以上是关于如何正确处理 Promise 链中的错误?的主要内容,如果未能解决你的问题,请参考以下文章

Promise 链中的 for 循环中的 Promise

如何在数组方法链中展开 Promise 数组?

如何在Promise链中共享变量?

Mocha 抛出超时,但 Mongoose 将数据保存到 Promise 链中的数据库?

使用 redux-promise 处理 redux 错误的正确方法

如何在 reducer 中处理 Redux 的 redux-promise 中间件 AJAX 错误?