处理承诺链中的多个捕获

时间:2014-09-27 16:02:49

标签: javascript node.js promise bluebird

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

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

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的情况。

任何帮助都会很棒。

8 个答案:

答案 0 :(得分:119)

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

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

这样,逻辑就在一个地方,如何处理客户端错误的决定都在一个地方,而且它们不会混在一起。

答案 1 :(得分:44)

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

答案 2 :(得分:16)

  

我想知道是否有办法让我以某种方式强迫链条根据错误在某一点停止

没有。你不能真的"结束"一个链,除非你抛出一个泡沫直到它结束的异常。有关如何执行此操作,请参阅Benjamin Gruenbaum's answer

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

  

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

是的,你可以branching with 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" });
});

答案 3 :(得分:3)

我一直这样做:

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

    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 :(得分:2)

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

Mozilla Developer Network - Using Promises

编辑:之所以提交此文件,是因为它通常提供所需的功能。但是,在这种特殊情况下却不是。因为正如其他人已经详细解释的那样,所以.catch应该可以修复该错误。例如,您无法通过多个 .catch回调向客户端发送响应,因为没有明确.catch return可以解决undefined结合使用,则.then会触发.catch触发,即使您的链没有真正解决,也可能导致随后的UnhandledPromiseRejection触发并向客户端发送另一个响应,导致错误,并有可能抛出{{1}}。我希望这个复杂的句子对您有意义。

答案 5 :(得分:0)

而不是.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)与最近的错误进行交互有点骇客。

答案 6 :(得分:0)

我认为Benjamin Gruenbaum's answer above是复杂逻辑序列的最佳解决方案,但这是我在较简单情况下的替代方案。我只使用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我们需要跳过,它将被视为未处理的Promise拒绝,Node不喜欢。如上所述,最后的catch将返回一个和平解决的承诺。

答案 7 :(得分:0)

我想保留 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 代码来实现上述内容