【问题标题】:Handling multiple catches in promise chain处理承诺链中的多个捕获
【发布时间】:2014-11-22 11:10:14
【问题描述】:

我对 Promise 还很陌生,目前正在使用 bluebird,但是我有一个场景,我不太确定如何最好地处理它。

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

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

任何帮助都会很棒。

【问题讨论】:

  • 可以重投还是提前归还?

标签: javascript node.js promise bluebird


【解决方案1】:

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

如果你能处理使这段代码工作的机制中的一些丑陋,那么结果是一个干净的代码结构,类似于非嵌套链式.then()'s

像这样构建链的一个很好的部分是,您可以通过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 代码

【讨论】:

    【解决方案2】:

    聚会可能有点晚了,但可以嵌套.catch,如下所示:

    Mozilla Developer Network - Using Promises

    编辑:我提交这个是因为它提供了一般要求的功能。但是,在这种特殊情况下并非如此。因为正如其他人已经详细解释的那样,.catch 应该可以恢复错误。例如,您不能在 多个 .catch 回调中向客户端发送响应,因为 .catch 没有明确的 return 解决它与 @987654327 @ 在这种情况下,即使您的链没有真正解决,也会导致继续进行 .then 触发,这可能会导致后续 .catch 触发并向客户端发送另一个响应,从而导致错误并可能以您的方式抛出 UnhandledPromiseRejection .我希望这个复杂的句子对你有一些意义。

    【讨论】:

    • @AntonMenshov 你是对的。我扩展了我的答案,解释了为什么嵌套仍然无法实现他想要的行为
    【解决方案3】:

    我认为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 将返回一个和平解决的 Promise。

    【讨论】:

      【解决方案4】:

      此行为与同步抛出完全相同:

      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
      

      但是,仅此一项不适用于您的情况,因为错误会被稍后的处理程序捕获。这里真正的问题是通用的“处理任何事情”错误处理程序通常是一种不好的做法,并且在其他编程语言和生态系统中非常不受欢迎。出于这个原因,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" });
       });
      

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

      【讨论】:

      • 您可能想补充一点,为某些特定错误使用中间 .catch(someSpecificError) 处理程序的原因是,如果您想捕获特定类型的错误(无害),处理它并继续接下来的流程。例如,我有一些启动代码有一系列事情要做。第一件事是从磁盘读取配置文件,但如果该配置文件丢失,这是一个 OK 错误(程序已内置默认值),因此我可以处理该特定错误并继续其余流程。也可能有清理工作,最好不要等到以后再离开。
      • 我认为“这是 .catch 的一半——能够从错误中恢复”已经很清楚了,但感谢您进一步澄清这是一个很好的例子。
      • 如果没有使用蓝鸟怎么办?普通的 es6 承诺只有一个字符串错误消息传递给 catch。
      • @clocksmith 与 ES6 承诺你会被困在捕捉所有东西并自己手动执行 instanceofchceks。
      • 对于那些寻找子类化错误对象参考的人,请阅读bluebirdjs.com/docs/api/catch.html#filtered-catch。文章也几乎再现了这里给出的多重捕获答案。
      【解决方案5】:

      您可以使用.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) 是一个与最近的错误交互的小技巧。

      【讨论】:

        【解决方案6】:

        我一直是这样的:

        你最终还是留下了你的收获。当它发生在你的链条中间时,就抛出一个错误。

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

        【讨论】:

          【解决方案7】:

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

          没有。你不能真正“结束”一个链,除非你抛出一个冒泡直到结束的异常。请参阅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" });
          });
          

          【讨论】:

            【解决方案8】:

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

            【讨论】:

            • 是的,我知道这一点,但我不想做一个巨大的错误链,而且在需要时这样做似乎更具可读性。因此,最后抓住了一切,但我喜欢输入错误的想法,因为它更能描述意图。
            • @Grofit 的价值 - 在 Bluebird 中键入捕获 Petka (Esailija) 的想法:) 无需说服他他们是更可取的方法这里。我想他不想让你感到困惑,因为很多 JS 的人都不太了解这个概念。
            • 我必须在then() 之一中使用if (!res.ok)throw new Error() 吗?
            • 可能是throw .. creates an error
            猜你喜欢
            • 2021-11-06
            • 1970-01-01
            • 2017-03-03
            • 2020-11-01
            • 2018-11-07
            • 1970-01-01
            • 2018-02-19
            • 1970-01-01
            相关资源
            最近更新 更多