Node.js和Express中使用Promise进行错误处理

4
使用Node.js + Express (4) + Mongoose(使用Promise而不是回调函数),我无法解决如何整理我的错误处理。
我有以下代码(相当简单):
app.get('/xxx/:id', function(request, response) {
    Xxx.findById(request.params.id).exec()
        .then(function(xxx) {
            if (xxx == null) throw Error('Xxx '+request.params.id+' not found');
            response.send('Found xxx '+request.params.id);
        })
        .then(null, function(error) { // promise rejected
            switch (error.name) {
                case 'Error':
                    response.status(404).send(error.message); // xxx not found
                    break;
                case 'CastError':
                    response.status(404).send('Invalid id '+request.params.id);
                    break;
                default:
                    response.status(500).send(error.message);
                    break;
            }
        });
});

在“promise rejected”部分的switch语句中,Error是我自己抛出的错误,表示未找到可能有效的id,CastError是由Mongoose抛出的Cast to ObjectId failed无效id错误,而500错误可以由于将throw Error()误写为throw Err()(导致ReferenceError: Err is not defined)等原因触发。

但是这样,我的每个路由都有一个笨重的大开关来处理不同的错误。

如何集中处理错误?能否将开关放入某些中间件中?

(我希望在“promise rejected”块中使用throw error;重新抛出错误,但我没有成功过。)


你不能只把错误处理函数设为全局的(因为每个路由使用的是同一个),然后在每个路由中通过 .then(null, …) 进行传递吗? - Bergi
@Bergi:谢谢 - 我一直在想全局错误处理函数的事情,只是不知道是否有更“本地”的方法来做这件事。全局错误处理函数很好用 - 如果你把它作为答案,我也会接受的! - ChrisV
@ChrisV,你所说的更“本地”的方法是使用基于Promise的路由器,而不是默认的路由器,这将让你return Xxx.findById(...,然后检查拒绝。我想Spion在某个时候写过一个这样的路由器。 - Benjamin Gruenbaum
2个回答

7

我会创建中间件来处理错误。对于404错误使用next(),对于其他错误使用next(err)

app.get('/xxx/:id', function(req, res, next) {
  Xxx.findById(req.params.id).exec()
    .then(function(xxx) {
      if (xxx == null) return next(); // Not found
      return res.send('Found xxx '+request.params.id);
    })
    .then(null, function(err) {
      return next(err);
    });
});

404处理程序

app.use(function(req, res) {
  return res.send('404');
});

错误处理程序

app.use(function(err, req, res) {
  switch (err.name) {
    case 'CastError':
      res.status(400); // Bad Request
      return res.send('400');
    default:
      res.status(500); // Internal server error
      return res.send('500');
  }
});

您可以通过发送类似以下的json响应来进一步改进:

您可以通过发送如下的JSON响应进一步完善此功能:

return res.json({
  status: 'OK',
  result: someResult
});

或者

return res.json({
  status: 'error',
  message: err
});

2
当我按照你的建议添加return next(err);时,代码执行到该语句后就一直停留在“等待本地主机...”状态。如果你确信这种方法应该可行,也许我的代码还有其他问题,但是我真的看不出来哪里出了问题! - ChrisV
1
那么对于可能引入泄漏引用的意外编程错误(如ReferenceError),该怎么办呢?在这种情况下,官方文档建议重新启动进程,但是您实现的中间件将使节点保持活动状态。 - bpceee
1
@bpcee 很好的观点,应该更多地利用中间件来捕获可能发生的错误并抛出其他错误。 - Jordonias
1
很不幸,如果你在中间件中简单地抛出一个错误,由于Express将每个函数(req、res、next) {}封装在try/catch中,Node进程将不会崩溃。仍在努力寻找优雅的解决方案...... 如果您有任何想法,请告诉我。 - bpceee
你是使用 return next(err) 还是 throw err - OMGPOP
显示剩余4条评论

0

最近我需要进行错误处理。因此,我查阅了很多资源,最终想出了这个方法。我们将创建一个自定义错误类ErrorHandler中间件。自定义错误类用于动态发送其他详细信息,如statusCode和errMsg。而中间件用于集中处理所有错误,基本上会在控制台上显示完整的错误并发送错误响应。

  1. 创建自定义的 Err

    class Err extends Error {
       statusCode = 500;
       name = "InternalError";
       err = "Error";
    
       constructor(message, options = {}) {
         super(message);
    
         for (const [key, value] of Object.entries(options)) {
            this[key] = value;
         }
       }
    }
    

Err类接受以下内容,与内置的Error类只接受message不同。

  • message:您想向客户端展示的任何内容
  • options:它可以包含与错误相关的附加信息,如
    • err(实际错误)
    • name(自定义/实际错误名称)
    • statusCode(如400、404等)
  • 创建一个中间件ErrorHandler

    const errorHandler = (err, req, res, next) => {
       console.error(err);
    
       let errStatus = err.statusCode || 500; 
       let errMsg = err.message;
    
       //处理一些基本的mongodb错误
       if(err.name === 'CastError') {
         errMsg = `资源未找到。无效的:${err.path}`;
         errStatus = 400;
       } else if(err.name === 'ValidationError') {
         errMsg = `输入无效:${Object.values(err.errors).map(e => e.message)}`;
         errStatus = 400;
       } else if(err.code === 11000) {
         errMsg = `重复输入${Object.keys(err.keyValues)}`;
         errStatus = 403;
       }
       //您可以处理更多内置或基本错误,例如与jwt相关的错误等
    
       return res.status(errStatus).json({
         success: false,
         status: errStatus,
         message: errMsg,
         stack: process.env.ENV === 'DEV' ? err.stack : {}
       })
    }
    
  • 现在,错误处理就是小菜一碟。

    • 每当我们想要抛出一个自定义错误时,我们可以这样做:

       const err = throw new Err("Leaderboard not exist for this quiz", {
          err: "RedisError", 
          name: "EmptySetError", 
          statusCode: 422
       });
      

      [注意:不需要发送所有选项,如果您想要,只能发送statusCode。]

    • 或者,如果我们从 try...catch 块中捕获错误

       try {
         //像 await 调用那样执行你的工作
       } catch (err) {
      
       }
      

    我们可以像这样修改控制器

    const ctrl = (req, res, next) => {
       //err is either custom or we are catching from try...catch
       next(err);
    }
    

    我们将在您的index.js文件中的所有路由最后添加此中间件。

    app.routes....
    app.use(ErrorHandler);
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接