在Promises中实现错误区分

时间:2017-04-01 17:11:18

标签: javascript node.js rest express promise

背景

我有一个使用MongoDB,Node.js和Express的REST API向我的NoSQL DB发出请求,并且根据不同的结果,我想区分我发送给客户的错误。

问题

我的代码的当前版本有一个通用错误处理程序,并始终向客户端发送相同的错误消息:

api.post("/Surveys/", (req, res) => {
        const surveyJSON = req.body;
        const sender = replyFactory(res);

        Survey.findOne({_id: surveyJSON.id})
            .then(doc => {
                if(doc !== null)
                    throw {reason: "ObjectRepeated"};

                //do stuff
                return new Survey(surveyJSON).save();
            })
            .then(() => sender.replySuccess("Object saved with success!")) 
            .catch(error => {
                /*
                 * Here I don't know if:
                 * 1. The object is repeated
                 * 2. There was an error while saving (eg. validation failed)
                 * 3. Server had a hiccup (500)
                 */
                sender.replyBadRequest(error);    
            });
    });

这是一个问题,因为客户端总是会收到相同的错误消息,无论如何,我需要区分错误!

研究

我找到了一个可能的解决方案,基于逻辑划分和错误/响应处理:

但是,我不了解一些事情:

  1. 至少在我的例子中,我不知道如何将逻辑与响应区分开来。毕竟的响应将取决于
  2. 我想避免错误子类和层次结构。首先是因为我没有使用蓝鸟,我不能将答案所暗示的错误类子类化,其次是因为我不希望我的代码有十亿种不同的错误类别,这些错误类别将会改变脆弱的层次结构在将来。
  3. 我的想法是,我不喜欢

    使用这种结构,如果我想要区分错误,我唯一能做的就是检测发生的错误,用该信息构建一个对象然后抛出它:

    .then(doc => {
        if(doc === null)
            throw {reason: "ObjectNotFound"};
    
        //do stuff
        return doc.save();
    })
    .catch(error => {
        if(error.reason === "ObjectNotFound")  
            sendJsonResponse(res, 404, err);
        else if(error.reason === "Something else ")
            sendJsonResponse(/*you get the idea*/);
        else //if we don't know the reasons, its because the server likely crashed
            sendJsonResponse(res, 500, err);
    });
    

    我个人认为这个解决方案特别有吸引力,因为这意味着我的if then else块中会有一个巨大的catch语句链。

    另外,正如前一篇文章中所提到的,通用错误处理程序通常不受欢迎(并且有充分的理由)。

    问题

    如何改进此代码?

2 个答案:

答案 0 :(得分:2)

目标

当我开始这个主题时,我有两个目标:

  1. 有错误区分
  2. 避免在通用捕手中if then else厄运
  3. 我现在提出了两个截然不同的解决方案,我现在在这里发布,供将来参考。

    解决方案1:带有错误对象的通用错误处理程序

    此解决方案基于@Marc Rohloff的解决方案,但是,我没有一个函数数组并循环遍历每个函数,而是有一个包含所有错误的对象。

    这种方法更好,因为它更快,并且不需要if验证,这意味着你实际上做的逻辑更少:

    const errorHandlers = {
        ObjectRepeated: function(error){
            return { code: 400, error };
        },
        SomethingElse: function(error){
            return { code: 499, error };
        }
    };
    
    Survey.findOne({
            _id: "bananasId"
        })
        .then(doc => {
    
            //we dont want to add this object if we already have it
            if (doc !== null)
                throw { reason: "ObjectRepeated", error:"Object could not be inserted because it already exists."};
    
            //saving empty object for demonstration purposes
            return new Survey({}).save();
        })
        .then(() => console.log("Object saved with success!"))
        .catch(error => {
            respondToError(error);
        });
    
        const respondToError = error => {
            const errorObj = errorHandlers[error.reason](error);
    
            if (errorObj !== undefined)
                console.log(`Failed with ${errorObj.code} and reason ${error.reason}: ${JSON.stringify(errorObj)}`);
            else 
                //send default error Obj, server 500
                console.log(`Generic fail message ${JSON.stringify(error)}`);
        };
    

    此解决方案实现:

    1. 部分错误区分(我将解释原因)
    2. 避免if then else厄运。
    3. 此解决方案仅具有部分错误区分。原因是您只能通过throw {reaon: "reasonHere", error: "errorHere"}机制区分您专门构建的错误。

      在这个例子中,您将能够知道文档是否已经存在,但是如果保存所述文档时出现错误(比如说,验证文档),那么它将被视为“通用”错误并抛出为一个500.

      要实现完全错误区分,您必须使用nested Promise anti pattern,如下所示:

      .then(doc => {
      
          //we dont want to add this object if we already have it
          if (doc !== null)
              throw { reason: "ObjectRepeated", error:"Object could not be inserted because it already exists." };
      
          //saving empty object for demonstration purposes
          return new Survey({}).save()
              .then(() => {console.log("great success!");})
              .catch(error => {throw {reason: "SomethingElse", error}});
      })
      

      它会起作用......但我认为这是避免反模式的最佳做法。

      解决方案2:通过co

      使用ECMA6生成器

      此解决方案通过库Generators使用co。意味着在不久的将来使用类似于async/await的语法替换Promise这个新功能允许您编写读取类似同步的异步代码(好吧,差不多)。

      要使用它,首先需要安装co或类似ogen之类的内容。我非常喜欢co,所以这就是我将在这里使用的。

      const requestHandler = function*() {
      
          const survey = yield Survey.findOne({
              _id: "bananasId"
          });
      
          if (survey !== null) {
              console.log("use HTTP PUT instead!");
              return;
          }
      
          try {
              //saving empty object for demonstration purposes
              yield(new Survey({}).save());
              console.log("Saved Successfully !");
              return;
          }
          catch (error) {
              console.log(`Failed to save with error:  ${error}`);
              return;
          }
      
      };
      
      co(requestHandler)
          .then(() => {
              console.log("finished!");
          })
          .catch(console.log);
      

      生成器函数requestHandleryield所有对库的Promise,它将解析它们并相应地返回或抛出。

      使用此策略,您可以像编写同步代码一样有效地编写代码(使用yield除外)。

      我个人更喜欢这种策略,因为:

      1. 您的代码易于阅读且看起来是同步的(虽然仍然具有异步代码的优势)。
      2. 必须在每个地方构建和抛出错误对象,您只需立即发送邮件。
      3. 并且,您可以通过return BREAK 代码流。这在承诺链中是不可能的,因为在那些你必须强制throw(很多次是无意义的)并抓住它停止执行的那些。
      4. 生成器函数只有在传递到库co后才会执行,然后返回一个Promise,说明执行是否成功。

        此解决方案实现:

        1. 错误分化
        2. 避免if then else地狱和广义捕获者(虽然您将在代码中使用try/catch,但如果需要,您仍然可以访问广义捕手。)
        3. 在我看来,使用生成器更灵活,更易于阅读代码。并非所有情况都是发电机使用的情况(如视频中的mpj建议),但在这种特殊情况下,我认为这是最好的选择。

          结论

          解决方案1 ​​:对问题采取良好的经典方法,但有承诺链接所固有的问题。你可以通过嵌套承诺来克服其中的一些,但这是一种反模式并且无法实现它们的目的。

          解决方案2 :功能更多,但需要一个库和生成器如何工作的知识。此外,不同的库会有不同的行为,所以你应该意识到这一点。

答案 1 :(得分:0)

我认为一个好的改进是创建一个错误实用程序方法,它将错误消息作为参数,然后执行所有ifs尝试解析错误(必须在某处发生的逻辑)并返回格式化错误。

function errorFormatter(errMsg) {
   var formattedErr = {
    responseCode: 500,
    msg: 'Internal Server Error'
};

  switch (true) {
     case errMsg.includes('ObjectNotFound'):
      formattedErr.responseCode = 404;
      formattedErr.msg = 'Resource not found';
      break;
  }
  return formattedErr;
}