关于javascript:处理promise链中的多个catch

Handling multiple catches in promise chain

我仍然是相当新的承诺,目前正在使用蓝鸟,但我有一个场景,我不太确定如何最好地处理它。

例如,我在快递应用程序中有一个承诺链,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的情况。

任何帮助都会很棒。


此行为与同步投掷完全相同:

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

这是.catch点的一半 - 能够从错误中恢复。可能需要重新抛出以表明状态仍然是错误:

1
2
3
4
5
6
7
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"错误处理程序通常是一种不好的做法,并且在其他编程语言和生态系统中非常不受欢迎。出于这个原因,Bluebird提供了类型和谓词捕获。

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

这是我编写代码的方式。

首先,我得.Query来抛出NoSuchAccountError,我将它从Bluebird已经提供的Promise.OperationalError中继承。如果您不确定如何将错误子类化,请告诉我。

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

1
2
3
4
5
6
function changePassword(queryDataEtc){
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

正如您所看到的 - 它非常干净,您可以阅读文本,就像过程中发生的事情的说明手册一样。它也与请求/响应分开。

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

1
2
3
4
5
6
7
8
9
10
 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" });
 });

这样,逻辑就在一个地方,如何处理客户端错误的决定都集中在一个地方,并且它们不会相互混乱。


.catch的工作方式与try-catch语句类似,这意味着您最后只需要一个catch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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" });
            }
        });


I am wondering if there is a way for me to somehow force the chain to stop at a certain point based upon the errors

不可以。你不能真正"结束"一个链,除非你抛出一个泡沫直到结束的例外。请参阅Benjamin Gruenbaum关于如何做到这一点的答案。

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

or if there is a better way to structure this to get some form of branching behaviour

是的,你可以用promises做分支。但是,这意味着离开链并"返回"嵌套 - 就像你在嵌套的if-else或try-catch语句中所做的那样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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" });
});

我一直这样做:

你最后留下了你的渔获物。当它发生在你的链中间时发出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    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" });

    }
});

您的其他功能可能如下所示:

1
2
3
4
5
6
7
8
9
function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                      
}

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

Mozilla开发者网络 - 使用Promises

编辑:我提交了这个,因为它提供了一般的问题功能。 但是在这种特殊情况下并不存在。 因为已经有其他人详细解释过,.catch应该可以恢复错误。 例如,您不能在多个.catch回调中向客户端发送响应,因为在这种情况下,没有显式return.catchundefined解析它,导致继续.then触发,即使 你的链没有真正解决,可能导致跟随.catch触发并向客户端发送另一个响应,导致错误并可能以你的方式抛出UnhandledPromiseRejection。 我希望这句令人费解的句子对你有意义。


我认为Benjamin Gruenbaum上面的答案是复杂逻辑序列的最佳解决方案,但这是我对更简单情况的替代方案。我只是使用errorEncountered标志和return Promise.reject()来跳过任何后续的thencatch语句。所以它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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对,你应该使用Benjamin Gruenbaum的解决方案。但这适用于简单的设置。

请注意,最终的catch只有return;而不是return Promise.reject();,因为我们不需要跳过后续的then,它将被视为未处理的Promise拒绝,Node不喜欢。如上所述,最终catch将返回一个和平解决的Promise。


而不是.then().catch()...,你可以做.then(resolveFunc, rejectFunc)。如果您沿途处理事情,这个承诺链会更好。以下是我将如何重写它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)与最近的错误进行交互有点麻烦。