关于Nodejs异常处理的一个教训,再次理解什么是异步回调.

关于Nodejs异常处理的一个教训,再次理解什么是异步回调.

环境:express+sequelize 代码的意图是统一处理router中的异常,想当然的写了段try/catch,

var m = ['get','post','put','delete'];
m.forEach(function(type){
  var handlers = app.routes[type];
  if(handlers){
    handlers.forEach(function(handler){
      //TODO:need to config the excluded handler path,not the hard code. 
      if(handler.path !== '/' && handler.path !== '/auth'){//the '/auth' is the login url.
        handler.callbacks = [authIdentity].concat(handler.callbacks);
      }
      handler.callbacks.forEach(function(callback){
        var packageCallBack = function(req,res,next){
          try{callback(req,res,next);}
          catch(e){
            debugger;
            //TODO:need to implement other logics.  
            if(e instanceof AppError){
              res.send(e);
            }
            next(e);
          }
        };
        callback = packageCallBack;
      });
    });
  }
});

在处理请求的方法中,想当然的抛异常.

exports.authenticate= function(req, res, next){
  var daos = app.get('models')
    ,user = daos.user
    ,_vaule = {};
  user.find({where:{login:req.param('login')}}).success(
      function(value){
        if(value){
          var result = md5.validate(value.hashed_password,req.param('passwd'));
          if(result){
            req.session.userinfo = value.login;
            var url = req.param('url');
            if(url){
              res.redirect(url);
            }
          }else{
            throw new AppError('invalid user or passwd.');
            //next(new AppError('invalid user or passwd.'));
          }
        }
        else{
            throw new AppError('the user does not exist.');
          //next(new AppError('the user does not exist.'));
        }
      });
};

期望就是抛出的异常,有外围的try/catch去捕获,相信很多初学者都会犯类似的错误,在异步回调的时空里,回调方法抛出异常,往往就是噩梦,因为回调方法里的异常,try有啥用?

个人有个不恰当的比喻: node就像是一根绳子,异步回调是无处不在的绳子节点,抛异常好比剪刀,一刀下去,绳子断了. 最终的处理方式,已经很明显了,使用express中的next去传递异常,而不是抛~ 这里强调一下,回调和异步回调不是同一个概念,这里有篇文章讲解了回调捕获异常和异步回调捕获异常的区别 http://www.html-js.com/archives/category/nodejs/page/2

下面几个链接讲述了node中异常处理的基本方式: http://snmaynard.com/2012/12/21/node-error-handling/ http://stackoverflow.com/questions/7151487/error-handling-principles-for-nodejs-express-apps http://stackoverflow.com/questions/7310521/node-js-best-practice-exception-handling


2 回复

关于Nodejs异常处理的一个教训,再次理解什么是异步回调

在Node.js中,处理异常的方式与传统的同步代码有所不同。尤其是在涉及到异步回调时,异常处理变得更加复杂。本文通过一个实际的例子来说明这个问题,并提出解决方案。

环境

本例使用的是express框架配合sequelize ORM。

问题描述

我们的目标是在路由处理中统一处理异常。最初的想法是使用try/catch块来捕获异常:

var m = ['get', 'post', 'put', 'delete'];
m.forEach(function (type) {
  var handlers = app.routes[type];
  if (handlers) {
    handlers.forEach(function (handler) {
      if (handler.path !== '/' && handler.path !== '/auth') {
        handler.callbacks = [authIdentity].concat(handler.callbacks);
      }
      handler.callbacks.forEach(function (callback) {
        var packageCallback = function (req, res, next) {
          try {
            callback(req, res, next);
          } catch (e) {
            debugger;
            if (e instanceof AppError) {
              res.send(e);
            }
            next(e);
          }
        };
        callback = packageCallback;
      });
    });
  }
});

然而,在处理请求的方法中,我们发现即使在try/catch块内抛出异常,也无法被捕获:

exports.authenticate = function (req, res, next) {
  var daos = app.get('models'),
      user = daos.user,
      _value = {};

  user.find({ where: { login: req.param('login') } }).success(
    function (value) {
      if (value) {
        var result = md5.validate(value.hashed_password, req.param('passwd'));
        if (result) {
          req.session.userinfo = value.login;
          var url = req.param('url');
          if (url) {
            res.redirect(url);
          }
        } else {
          throw new AppError('invalid user or passwd.');
          // next(new AppError('invalid user or passwd.'));
        }
      } else {
        throw new AppError('the user does not exist.');
        // next(new AppError('the user does not exist.'));
      }
    }
  );
};

原因分析

在异步回调中抛出的异常并不会被try/catch捕获。这是因为异步回调是在事件循环的其他阶段执行的,而此时try/catch已经结束了。

解决方案

正确的做法是使用next函数将异常传递给Express中间件链。例如:

exports.authenticate = function (req, res, next) {
  var daos = app.get('models'),
      user = daos.user,
      _value = {};

  user.find({ where: { login: req.param('login') } }).success(
    function (value) {
      if (value) {
        var result = md5.validate(value.hashed_password, req.param('passwd'));
        if (result) {
          req.session.userinfo = value.login;
          var url = req.param('url');
          if (url) {
            res.redirect(url);
          }
        } else {
          return next(new AppError('invalid user or passwd.'));
        }
      } else {
        return next(new AppError('the user does not exist.'));
      }
    },
    function (err) {
      if (err) {
        return next(err);
      }
    }
  );
};

这样,Express会自动处理这些异常并调用相应的错误处理中间件。

总结

在Node.js中,处理异步回调中的异常需要特别注意。直接抛出异常通常不会被try/catch捕获,而是应该使用next函数来传递异常。这不仅符合Node.js的设计理念,还能更好地管理应用的错误处理逻辑。


在Node.js中,异步回调的异常处理与同步代码的处理方式不同。在同步代码中,可以使用try/catch来捕获异常,但在异步回调中,这种方式不起作用。这是因为当回调函数执行时,它所在的上下文已经被改变,而try/catch只能捕获当前上下文中的异常。

在你的例子中,尝试在authenticate方法的回调函数内部抛出异常,但这样会导致异常无法被捕获。正确的做法是在回调函数中调用next函数来传递异常,让Express框架来处理这些异常。

示例代码

const express = require('express');
const app = express();
const sequelize = require('sequelize');

app.use(express.json());

// 模拟模型
const models = {
  user: {
    find: (query, callback) => {
      const users = [{ id: 1, login: 'admin', hashed_password: 'hashed_admin_pass' }];
      const foundUser = users.find(user => user.login === query.where.login);
      callback(foundUser);
    }
  }
};

app.set('models', models);

const authIdentity = (req, res, next) => {
  next();
};

function authenticate(req, res, next) {
  const user = models.user;
  const value = {};

  user.find({ where: { login: req.body.login } }, (err, foundUser) => {
    if (err) return next(err);
    
    if (foundUser) {
      const isValidPassword = md5.validate(foundUser.hashed_password, req.body.password);
      if (isValidPassword) {
        req.session.userinfo = foundUser.login;
        const url = req.body.url;
        if (url) {
          res.redirect(url);
        }
      } else {
        return next(new AppError('invalid user or passwd.'));
      }
    } else {
      return next(new AppError('the user does not exist.'));
    }
  });
}

app.post('/login', authenticate, (req, res) => {
  res.send('Login successful');
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

const md5 = {
  validate: (hashed, input) => hashed === md5.hash(input)
};

md5.hash = (input) => {
  return 'hashed_' + input;
};

const AppError = (message) => {
  this.message = message;
  this.name = 'AppError';
};

const server = app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

解释

  1. authenticate 方法:在这个方法中,我们模拟了一个异步查找用户的过程。如果找到用户,则验证密码。如果密码无效或用户不存在,通过调用 next 函数并传递一个错误对象来传递异常。

  2. 错误处理中间件:在最后定义了一个错误处理中间件,用于捕获并处理所有未被捕捉到的异常。

通过这种方式,你可以确保在异步回调中抛出的异常能够被正确地处理。

回到顶部