承诺链中的多重挂钩处理

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

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

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" });
});

因此,我追求的行为是:

  • 用身份证开户
  • 如果此时出现拒绝,则删除并返回错误
  • 如果没有错误,则将返回的文档转换为模型
  • 使用数据库文档验证密码
  • 如果密码不匹配,那么弹出并返回一个不同的错误
  • 如果没有错误,请更改密码
  • 那就成功归来
  • 如果出了什么问题,退500块

因此,目前抓住似乎不能停止链接,这是有意义的,所以我想知道是否有一种方法,我以某种方式迫使链停止在某一点的基础上的错误,或者是否有一个更好的方法来构造这一点,以获得某种形式的分支行为,有一个案例的 if X do Y else Z

任何帮助都可以。

105494 次浏览

.catch的工作原理类似于 try-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" });
}
});

这种行为与同步抛掷完全相同:

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”错误处理程序通常是一种糟糕的做法,在其他编程语言和生态系统中极其不受欢迎。由于这个原因,Bluebird 提供了类型化和谓词捕获。

额外的好处是,您的业务逻辑根本不必(也不应该)了解请求/响应周期。决定客户端的 HTTP 状态和错误不是查询的责任,随着应用程序的增长,你可能需要将业务逻辑(如何查询数据库和如何处理数据)与发送给客户端的内容(什么 HTTP状态码、什么文本和什么响应)分开。

我会这样写你的代码。

首先,我让 .Query抛出一个 NoSuchAccountError,然后从 Bluebird 已经提供的 Promise.OperationalError中继承它。如果您不确定如何子类化一个错误,请让我知道。

我会额外为 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" });
});

这样,逻辑都在一个地方,如何向客户端处理错误的决策都在一个地方,它们不会相互混乱。

我想知道是否有一种方法,我以某种方式迫使链停止在某一点的基础上的错误

没有。您不能真正“结束”链,除非您抛出一个异常,该异常会冒泡直到其结束。请参阅 Benjamin Gruenbaum 的回答了解如何做到这一点。

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

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

是的,你可以做 许下诺言。然而,这意味着离开链并“返回”嵌套——就像在嵌套的 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" });
});

我一直是这样做的:

你在最后留下你的捕获。当它发生在你的链条中间时,只是抛出一个错误。

    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);
}
}

你可以用 .then(resolveFunc, rejectFunc)代替 .then().catch()...。如果你一路处理事情,这个承诺链会更好。我会这样重写:

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)有点像一个小技巧。

我认为 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 */
});

如果您有两个以上的 then/catch 对,您可能应该使用 Benjamin Gruenbaum 的解决方案。但是这对于一个简单的设置是有效的。

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

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

Mozilla Developer Network-使用承诺

编辑: 我提交这个是因为它提供了一般要求的功能。然而,在这个特殊的情况下,它不是。因为正如其他人已经详细解释过的那样,.catch应该能够恢复错误。例如,你不能在 多个 .catch回调函数中向客户端发送响应,因为在这种情况下,没有显式 return 决心.catchundefined一起触发,导致进行 .then触发,即使你的链没有真正解析,可能导致下一个 .catch触发并向客户端发送另一个响应,导致错误并可能抛出一个 UnhandledPromiseRejection。我希望这个令人费解的句子对你有些意义。

我想保留 Bergi 的答案所具有的分支行为,但仍然提供未嵌套 .then()的干净代码结构

如果您能够处理使这些代码工作的机器中的一些丑陋的东西,那么结果将是一个类似于非嵌套链式 .then()的干净的代码结构

构建这样的链的一个好的部分是,你可以通过 chainRequests(...).then(handleAllPotentialResults)在一个地方处理所有潜在的结果,如果你需要隐藏请求链在一些标准化的接口后面,这可能是好的。

const log = console.log;
const chainRequest = (stepFunction, step) => (response) => {
if (response.status === 200) {
return stepFunction(response, step);
}
else {
log(`Failure at step: ${step}`);
return response;
}
};
const chainRequests = (initialRequest, ...steps) => {
const recurs = (step) => (response) => {
const incStep = step + 1;
const nextStep = steps.shift();
return nextStep ? nextStep(response, step).then(chainRequest(recurs(incStep), incStep)) : response;
};
return initialRequest().then(recurs(0));
};
// Usage
async function workingExample() {
return await chainRequests(
() => fetch('https://jsonplaceholder.typicode.com/users'),
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/'); },
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
);
}
async function failureExample() {
return await chainRequests(
() => fetch('https://jsonplaceholder.typicode.com/users'),
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/fail'); },
(resp, step) => { log(`step: ${step}`, resp); return fetch('https://jsonplaceholder.typicode.com/posts/3'); }
);
}
console.log(await workingExample());
console.log(await failureExample());

这个想法是存在的,但是公开的界面可能需要一些调整。

由于该实现使用了局部箭头函数,因此可以使用更直接的 async/await代码来实现上述功能