Express async 全局错误处理

之前两篇 Promise 错误处理其实已经描述了大部分场景下的错误捕获问题。
结合 async/await 可以获得很好的开发体验。
express 也可以使用 async/await 来提升开发体验,但必须 try/catch 所有 await 才行。
那么今天的课题就是如何优雅的在 express 中使用 async/await 来提升开发体验。

Promise 错误

koa2 中,我们可以很容易的全局处理掉,因为他天生支持。
而 express 中则不是,全局的错误处理并不能捕获 Promise 错误。

1
2
3
4
router.get('/', async (req, res) => {
const user = await db.userInfo();
res.json(user);
});

如果 db.userInfo() 报错了,express 无法捕获到这个错误,会报一个 UnhandledPromiseRejectionWarning 全局错误。
我们不得不加上 try/catch 来捕获异常。

1
2
3
4
5
6
7
8
router.get('/', async (req, res, next) => {
try {
const user = await db.userInfo();
res.json(user);
} catch (err) {
next(err);
}
});

然后我们的路由中大量的 try/catch,不仅难看,写着也烦。
所以我们需要改善这块代码,提升开发体验。

捕获函数

1
2
3
4
5
6
7
8
9
const asyncHandler = fn => (req, res, next) =>
Promise.resolve()
.then(() => fn(req, res, next))
.catch(next);

router.get('/', asyncHandler(async (req, res) => {
const user = await db.userInfo();
res.json(user);
}));

可以看到,只要简单的几行代码,就可以处理掉错误问题了。
他会将错误捕获并交给 next 错误,这时候就会去到 express 全局错误中间件中。

但其实每个路由都加一个 asyncHandler 也不是特别优雅,或者不经意间忘记加了。

修改 express 路由层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Layer = require('express/lib/router/layer');

Object.defineProperty(Layer.prototype, 'handle', {
enumerable: true,
get() {
return this.__handle;
},
set(fn) {
if (fn.length === 4) {
this.__handle = fn;
} else {
this.__handle = (req, res, next) =>
Promise.resolve()
.then(() => fn(req, res, next))
.catch(next);
}
},
});

我们直接修改了路由层,只是把刚刚的 asyncHandler 代码放到了路由层中。
这样全局劫持了所有路由绑定,我们就可以非常正常的写我们的路由了。

这个只是个核心思想,推荐用完善的 express-async-errors 模块。

1
2
3
4
router.get('/', async (req, res) => {
const user = await db.userInfo();
res.json(user);
});

这才是我们想要的样子。

如果你觉得侵入式的修改 express 方法不是特别安全,那就使用上面的方法吧,至少那是绝对安全的。

小结

这里介绍了两种方法帮助我们在 express 中优雅的捕获错误,大幅度的提升了开发体验。
其实,我原先自己实现的方案中代码略微啰嗦,后来参考了国外大佬的文章,才显的这么优雅。
甚至方案二,我的实现是劫持 app 实例,代码非常的不优雅。

参考:
Using async/await in ExpressJS middlewares
ES6 generators support for ExpressJS