Node.js 中的异常
Node.js 跟 JavaScript一样,同步代码中的异常我们可以通过 try catch 来捕获.
异步回调异常
但异步代码呢? 我们来看一个 http server 启动的代码,这个也是个典型的异步代码。
const http = require('http')
try {
const server = http.createServer(function (req, res) {
console.log('来了')
throw new Error('hi')
res.end('helo')
})
server.listen(3002)
}
catch (err) {
console.log('出错了')
}
我们发现异步代码的异常无法直接捕获。这会导致 Node.js 进程退出。最明显的就是 web server 直接挂掉了。
异步代码也有解决办法,我们直接把try catch 写在异步代码的回调里面:
const http = require('http')
try {
const server = http.createServer(function (req, res) {
try {
throw new Error('hi')
}
catch (err) {
console.log('出错了')
}
res.end('helo')
})
server.listen(3002)
}
catch (err) {
console.log('出错了')
}
这样也能catch到错误。
然而业务代码非常复杂,并不是所有的情况我们都能预料到。比如在try...catch之后又出现一个throw Error.
所有没有catch 的Error都会往上冒泡直到变成一个全局的 uncaughtException。 Node.js里对未捕获的异常会检查有没有监听该事件,如果没有就把进程退出:
function _MyFatalException(err){
if(!process.emit('uncaughtException',err)){
console.error(err.stack);
process.emit('exit',1);
}
}
因此,防止异步回调异常导致进程退出的办法仿佛就是监听该事件
process.on('uncaughtException', function(err) {
console.log('出错了,我记录你,并吃掉你')
})
const http = require('http')
try {
const server = http.createServer(function (req, res) {
try {
throw new Error('hi')
}
catch (err) {
console.log('出错了')
}
throw new Error('有一个error')
res.end('helo')
})
server.listen(3002)
}
catch (err) {
console.log('出错了')
}
这样进程不会退出。但 极其不优雅
。 因为 uncaughtException 中没有了req和res上下文,无法友好响应用户。另外可能造成内存泄漏(具体参考网络其他资料)
因此,uncaughtException 适合用来做Node.js 整个应用最后的兜底。(记录日志or重启服务)
Promise的reject异常
如果使用了promise,且非异步reject了。在 Node.js 中,这个promise reject 行为会在控制台打印,但目前Node版本不会造成进程退出,也不会触发全局 uncaughtException.
promise最有争议的地方就是当一个promise失败但是没有rejection handler处理错误时静默失败。不过浏览器和Node.js都有相应的处理机制,两者大同小异,都是通过事件的方式监听. 有两个全局事件可以用来监听 Promise 异常:
- unhandledRejection:当promise失败(rejected),但又没有处理时触发,event handler 有2个参数: reason,promise;
- rejectionHandled: 当promise失败(rejected),被处理时触发,hanler 有1个参数: promise;
到底该如何处理异常
最好的处理方式,就是应该感知到自己业务代码中的异常。这样的话,无论业务开发人员自己处理了还是没处理,都能在应用上层catch到进行日志记录。 更佳的情况是:在感知到错误后,能给浏览器一些默认的提示。
可是业务代码里有同步有异步,如此复杂的代码如何能全部cover住呢?
这个会有一些技巧:比如假设我们的业务代码全部被包裹在自己的一个Promise中,且业务代码的每一个异步函数都可以被我们注入catch回调。在这样完美的情况下,我们就能在最外层捕获内部发生的所有异常了。
Koa 就是这么干的。Koa1 用 co来运行中间件,co就可以把generator运行起来且捕获其中的异步错误。想了解具体原理的,可能要去看更详细的资料了
Koa 中捕获异常和错误的机制
- 业务自己try catch
这种方式任何JavaScript程序都可以使用,是业务开发人员自己要做的。不多说了
- 写前置中间件
由于Koa是洋葱模型,因此可以在业务逻辑的前置中间件里捕获后面中间件的错误。这里是基于 yield 异步异常可以被try catch的机制。例如:
app.use(function *(next) {
try {
yield next;
} catch (err) {
console.log('哇哈 抓到一个错误')
// 友好显示给浏览器
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
});
实际上,上述中间件的工作 ctx.onerror 已经做了。 Koa 内核会自动把中间件的错误交给 ctx.onerror 处理,因此这个中间件我感觉没必要写了(除非要自定义这个默认的错误处理逻辑)。
- 监听app.on('error')
如果所有中间件都没有捕获到某个异常,那么co会捕获到。co会调用context对象的onerror, 从而做一些处理(例如返回给浏览器500错误),同时触发 app.onerror
因此,在app.onerror里,你可以做些日志记录或自定义响应
- uncaughtException
如果 Koa 都没有捕获到异常,那么就由Node来兜底了。不过这个一般不会发生,除非你在app.onerror里还要扔出异常(然而这是个promise异常,也不会触发uncaughtException)。
Koa错误处理最佳实践
- 抛出异常
在 Koa1 中间件里,你可以使用 this.throw(status, msg)
抛出异常。 Koa的底层其实本质上会使用 http-errors模块包装这个Error, 并直接 throw这个异常。
以下是 this.throw 函数源码:
// 将你传递的错误码和msg包装为一个 Error对象
throw: function(){
throw createError.apply(null, arguments);
}
其中 createError函数相当于:
var err = new Error(msg);
err.status = status;
throw err; // 包装后再抛出,ctx.onerror才能正确响应错误码给浏览器,否则都是500
因此 中间件 中你调用 this.throw 函数实际上就是真的 throw了一个异常,最终会导致 co 异常。
由于前文讲到的 Koa co 错误捕获机制(co-->catch-->ctx.onerror-->app.onerror),因此,你在任何中间件中throw的异常都可以被app.onerror捕获到。
- 逃逸的异常
co在运行generator时,如果某个yield右侧又是一个generator,那么co也会递归地去运行它。当然也会捕获这个嵌套的异步异常。但有些情况下嵌套异步会逃出一个异步的错误检测机制。
比如在Promise里做了另外一个异步操作, 在另外的异步操作里抛出了异常。
var fn = function () {
return new Promise (function (resolve, reject) {
setTimeout(function(){throw new Error('inner bad')})
})
}
这个异常,Promise就无法catch到。 同样,在generator里如果用了这样的方式,异常也会逃逸导致无法捕获。
问题:逃逸出Koa 的 co异步调用链的代码,会导致co无法catch异常。
不如去看看egg怎么做的吧