处理承诺链中的多个渔获


125

我对诺言仍然还很陌生,目前正在使用蓝鸟,但是在我不确定如何最好地处理它的情况下。

因此,举例来说,我在Express应用程序中有一个Promise链,如下所示:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

所以我的行为是:

  • 通过ID获取帐户
  • 如果此时存在拒绝,请炸毁并返回错误
  • 如果没有错误,则将文档转换为模型
  • 使用数据库文档验证密码
  • 如果密码不匹配,则炸开并返回其他错误
  • 如果没有错误,请更改密码
  • 然后返回成功
  • 如果还有其他问题,请返回500

因此,当前的捕获量似乎并没有停止链接,这是有道理的,所以我想知道是否存在一种方法可以根据错误以某种方式强制链停止在某个点,或者是否有更好的方法构造这种形式以获得某种形式的分支行为,例如if X do Y else Z

任何帮助都会很棒。


您可以重新投掷还是提前返回?
Pieter21

Answers:


126

此行为完全类似于同步抛出:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

.catch是能够从错误中恢复的一半。可能需要重新抛出信号以指示状态仍然是错误:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

但是,仅此一种方法对您而言不起作用,因为以后的处理程序会捕获该错误。真正的问题是,广义的“ HANDLE ANYTHING”错误处理程序通常是一种不好的做法,在其他编程语言和生态系统中却极少使用。因此,蓝鸟提供了类型化和谓词性的捕获。

附加的优点是您的业务逻辑根本不必(也不应该)知道请求/响应周期。决定客户端获取哪种HTTP状态和错误不是查询的责任,而随着应用的增长,您可能希望将业务逻辑(如何查询数据库以及如何处理数据)与发送给客户端的内容分开(什么http状态代码,什么文本和什么响应)。

这就是我编写您的代码的方式。

首先,我将.Query抛出一个NoSuchAccountError,将其从Promise.OperationalErrorBluebird已经提供的子类化。如果您不确定如何将错误归类,请告诉我。

我另外为其子类化AuthenticationError,然后执行以下操作:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

如您所见-它非常干净,您可以阅读文本,就像使用说明书一样,了解过程中发生的情况。它也与请求/响应分开。

现在,我将从路由处理程序中这样调用它:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

这样,逻辑就全部集中在一个地方,而如何处理客户端错误的决定就全部集中在一个地方,而且它们不会相互干扰。


11
您可能想要添加一个.catch(someSpecificError)针对某个特定错误的中间处理程序的原因是,如果您想捕获一种特定类型的错误(无害),对其进行处理并继续下面的流程。例如,我有一些启动代码,其中包含一系列要做的事情。第一件事是从磁盘读取配置文件,但是如果该配置文件丢失了,那是一个确定的错误(程序已内置默认值),因此我可以处理该特定错误并继续其余流程。最好也有清理工作,直到以后再离开。
jfriend00 2014年

1
我认为“这是.catch的一半-能够从错误中恢复”已经明确了这一点,但是感谢您进一步澄清,这是一个很好的例子。
Benjamin Gruenbaum 2014年

1
如果不使用蓝鸟怎么办?普通es6 Promise仅具有传递给catch的字符串错误消息。
Clocksmith

3
带有ES6的@clocksmith保证您无法instanceof亲自捕获所有内容并手动执行chceks。
本杰明·格林鲍姆

1
对于那些寻求对Error对象进行子类化的参考的人,请阅读 bluebirdjs.com/docs/api/catch.html#filtered-catch。文章还几乎重现了此处给出的多重捕获答案。
mummybot

47

.catch就像try-catch语句一样工作,这意味着最后只需要一个catch:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });

1
是的,我知道这一点,但是我不想做一个巨大的错误链,并且在需要时做起来似乎更具可读性。因此,最后发现了所有问题,但是我喜欢类型错误的想法,因为它更能说明意图。
Grofit

8
@Grofit的价值 -Petka(Esailija)的想法在Bluebird 键入渔获物:)无需说服他,这是这里的一种更好的选择。我想他不想让您感到困惑,因为JS中的很多人都不太了解这个概念。
Benjamin Gruenbaum 2014年

17

我想知道是否存在一种方法可以根据错误以某种方式强制链条停止在某个点

不会。您不能真正“终止”一条链,除非您抛出一个气泡直到其终止为止的异常。有关如何执行此操作的信息,请参见本杰明·格林鲍姆的答案

他的模式的派生将不是区分错误类型,而是使用具有statusCodebody字段的错误,这些错误和字段可以从单个通用.catch处理程序发送。根据您的应用程序结构,他的解决方案可能会更干净。

或者是否有更好的方法来构造这种形式以获得某种形式的分支行为

是的,您可以使用promises进行分支。但是,这意味着要离开链并“返回”嵌套-就像在嵌套的if-else或try-catch语句中那样:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});

5

我一直在这样做:

您最终将捕获的内容留了下来。当链中途发生错误时,就抛出一个错误。

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

您的其他函数可能看起来像这样:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}

4

派对可能要晚一点,但是可以嵌套.catch,如下所示:

Mozilla开发人员网络-使用承诺

编辑:我提交了此内容,因为它通常提供所需的功能。但是,在这种情况下并非如此。因为正如已经由其他人详细解释的那样,.catch应该恢复错误。例如,您不能在多个 .catch回调中向客户端发送响应,因为在这种情况下,如果.catch没有显式return 解析,则即使没有真正解决您的链,也会undefined导致继续.then触发,从而可能导致跟随.catch者触发并发送对客户端的另一个响应,从而导致错误,并有可能使UnhandledPromiseRejection您迷路。我希望这个复杂的句子对您有意义。


1
@AntonMenshov你是对的。我扩大了答案,解释了为什么仍然无法通过嵌套实现他期望的行为
denkquer

2

代替.then().catch()...你可以做.then(resolveFunc, rejectFunc)。如果您一路处理事情,那么这个承诺链会更好。这是我将其重写的方式:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

注:if (error != null)是一个黑客位的与最新的错误互动。


1

我认为上述本杰明·格鲁恩鲍姆(Benjamin Gruenbaum)的答案是复杂逻辑序列的最佳解决方案,但是这是我在较简单情况下的替代方案。我只是使用一个errorEncountered标志以及return Promise.reject()跳过任何后续thencatch声明。所以看起来像这样:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

如果您有两个以上的对/捕捉对,则可能应该使用本杰明·格伦鲍姆的解决方案。但这适用于简单的设置。

请注意,决赛catch仅具有return;而不是return Promise.reject();,因为我们没有随后的then需要跳过,并且它将被视为未处理的Promise拒绝,而Node则不喜欢。如上文所述,决赛catch将返回一个和平解决的承诺。

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.