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怎么做的吧


sheldon
947 声望1.6k 粉丝

echo sheldoncui


引用和评论

0 条评论