Koa入门教程[3]-错误和异常处理
Node.js 中的异常
Node.js 跟 JavaScript 一样,同步代码中的异常我们可以通过 try catch 来捕获.
异步回调异常
但异步代码呢? 我们来看一个 http server 启动的代码,这个也是个典型的异步代码。
1 | const http = require("http"); |
我们发现异步代码的异常无法直接捕获。这会导致 Node.js 进程退出。最明显的就是 web server 直接挂掉了。
异步代码也有解决办法,我们直接把 try catch 写在异步代码的回调里面:
1 | const http = require("http"); |
这样也能 catch 到错误。
然而业务代码非常复杂,并不是所有的情况我们都能预料到。比如在 try…catch 之后又出现一个 throw Error.
所有没有 catch 的 Error 都会往上冒泡直到变成一个全局的 uncaughtException。 Node.js 里对未捕获的异常会检查有没有监听该事件,如果没有就把进程退出:
1 | function _MyFatalException(err) { |
因此,防止异步回调异常导致进程退出的办法仿佛就是监听该事件
1 | process.on("uncaughtException", function (err) { |
这样进程不会退出。但 极其不优雅
。 因为 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 的机制。例如:
1 | app.use(function* (next) { |
实际上,上述中间件的工作 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 函数源码:
1 | // 将你传递的错误码和msg包装为一个 Error对象 |
其中 createError 函数相当于:
1 | var err = new Error(msg); |
因此 中间件 中你调用 this.throw 函数实际上就是真的 throw 了一个异常,最终会导致 co 异常。
由于前文讲到的 Koa co 错误捕获机制(co–>catch–>ctx.onerror–>app.onerror),因此,你在任何中间件中 throw 的异常都可以被 app.onerror 捕获到。
- 逃逸的异常
co 在运行 generator 时,如果某个 yield 右侧又是一个 generator,那么 co 也会递归地去运行它。当然也会捕获这个嵌套的异步异常。但有些情况下嵌套异步会逃出一个异步的错误检测机制。
比如在 Promise 里做了另外一个异步操作, 在另外的异步操作里抛出了异常。
1 | var fn = function () { |
这个异常,Promise 就无法 catch 到。 同样,在 generator 里如果用了这样的方式,异常也会逃逸导致无法捕获。
问题:逃逸出 Koa 的 co 异步调用链的代码,会导致 co 无法 catch 异常。
不如去看看egg怎么做的吧