Express异常捕获

在使用Express框架设计web应用时,一定会遇到异常处理问题。
因为用户数据或代码自身问题,常常引起请求时,控制器内部异常,res返回不能被执行,前台表现为请求“卡死”。

1 处理 Exception

1.1 Express 错误处理中间件

为了避免这种情况,Express有“错误处理中间件”的机制。

const express = require('express');

process.on('unhandledRejection', rej => console.warn('全局捕获Rejection', rej));

const app = express();

const router = express.Router();

router.get('/', function (req, res) {
    JSON.parse('{{'); // 抛出异常
    res.send('首页');
});

app.use(router);

// 错误处理中间件
app.use(function (err, req, res, next) {
  console.warn('错误处理中间捕获Exception', err);
  res.send('内部错误');
});

app.listen(5000, () => console.log('Server Start ...'));

上述代码运行后,访问http://localhost:5000时,会返回“内部错误”,表示控制器内错误被错误处理中间件捕获。

2 处理 Rejection

2.1 存在的问题

AsyncFunction内部异常时,不会抛出Exception,而是Rejection,不能被传统的try…catch…方式捕获。
上述代码如果改为:

// function => AsyncFunction
router.get('/', async function (req, res) {
    JSON.parse('{{'); // 抛出异常
    res.send('首页');
});

这时,访问主页,会发现卡死,并且后台打印“全局捕获Rejection…”。这表示异常不能被错误处理中间件捕获,而是在全局被捕获了。没有人处理请求的返回了,于是请求卡死。

2.2 异常处理装饰器

ctrlRejHandler.js :

module.exports = (ctrl) => {
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') throw Error('Try to use ctrlErrHandler to decorate an Object not a AsyncFunction');
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      console.warn('错误处理装饰器 捕获错误', rej);
      res.send('内部错误');
    });
  };
};
2.3 异常处理装饰器的位置问题

理论上,我们应该为每一个AsyncFunction类型的装饰器都使用异常处理。也就是说,我们希望尽量在全局安装装饰器,而不是每次使用控制器时都添加。
一种更简约的写法是,在使用module.exports导出控制器时,包装整个函数参数对象。但这会带来一个问题:无法通过IDE的定位功能定位ctrl了。

容易想到,控制器就是用在路由中的,至少路由使用控制器时,默认进行包装就好了。
可以定义一个函数来包装router,实现这样的功能。更改router.get/post/…等使用控制器的函数,对它们的最后一个回调函数参数(即控制器)进行错误处理。

routerRejHandler.js :

module.exports = routerRejHandler;

// 对ctrlRejHandler稍作修改,使其支持一般的function
function ctrlRejHandler(ctrl) {
  if (typeof ctrl !== 'function') throw Error('Try to use ctrlRejHandler to decorate an Object not a function');
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
    return ctrl;
  }
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      console.warn('错误处理装饰器 捕获错误', rej);
      res.send('内部错误');
    });
  };
};

// router装饰器,自动对控制器进行错误捕获
function routerRejHandler(router) {
  const methods = require('methods');
  [...methods, 'all'].forEach(method => {
    if (router[method]) {
      router[method] = function (path, ...fns) {
        const route = this.route(path);
        const ctrlIndex = fns.length - 1;
        // 只认为最后一个回调函数参数为控制器,之前的为中间件
        fns[ctrlIndex] = ctrlRejHandler(fns[ctrlIndex]);
        route[method].apply(route, fns);
        return this;
      };
    }
  });
  return router;
}

使用之:

const express = require('express');
const router = routerRejHandler(express.Router());

这时,对router.get('/path', 中间件1, 中间件2, 控制器)这样的情况,无需做任何其他处理,控制器抛出的Rejection将会被自动捕获和处理。

进一步的,可以为routerRejHandler增加一个回调函数,用以控制如何处理错误。

2.4 最佳实践

rejHandler.js :

module.exports = {
  ctrlDecorator,
  routerDecorator
};

function ctrlDecorator(ctrl, dealer) {
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
    return ctrl;
  }
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      if (dealer) return dealer(req, res, next, rej);
      res.send('Internal Error');
    });
  };
}

function routerDecorator(router, dealer) {
  const methods = require('methods');
  [...methods, 'all'].forEach(method => {
    if (router[method]) {
      router[method] = function (path, ...fns) {
        if (fns.length === 0) return;
        const route = this.route(path);
        const ctrlIndex = fns.length - 1;
        if (typeof fns[ctrlIndex] !== 'function') throw Error('The last param should be a controller, but not a function');
        fns[ctrlIndex] = ctrlDecorator(fns[ctrlIndex], dealer);
        route[method].apply(route, fns);
        return this;
      };
    }
  });
  return router;
}

使用:

const routerRejHandler = require('./rejHandler').routerDecorator;

const router = routerRejHandler(express.Router(), function (req, res, next, rej) {
  console.warn('捕捉到控制器内Rejection', rej);
  res.status(500);
  res.send('内部错误');
});

另外,若不使用router,装饰器同样可以用于app对象。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值