Koa入门教程[3]-错误和异常处理

Node.js 中的异常

Node.js 跟 JavaScript 一样,同步代码中的异常我们可以通过 try catch 来捕获.

异步回调异常

但异步代码呢? 我们来看一个 http server 启动的代码,这个也是个典型的异步代码。

1
2
3
4
5
6
7
8
9
10
11
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 写在异步代码的回调里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 里对未捕获的异常会检查有没有监听该事件,如果没有就把进程退出:

1
2
3
4
5
6
function _MyFatalException(err) {
if (!process.emit("uncaughtException", err)) {
console.error(err.stack);
process.emit("exit", 1);
}
}

因此,防止异步回调异常导致进程退出的办法仿佛就是监听该事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 的机制。例如:

1
2
3
4
5
6
7
8
9
10
11
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 函数源码:

1
2
3
4
// 将你传递的错误码和msg包装为一个 Error对象
throw: function(){
throw createError.apply(null, arguments);
}

其中 createError 函数相当于:

1
2
3
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 里做了另外一个异步操作, 在另外的异步操作里抛出了异常。

1
2
3
4
5
6
7
var fn = function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
throw new Error("inner bad");
});
});
};

这个异常,Promise 就无法 catch 到。 同样,在 generator 里如果用了这样的方式,异常也会逃逸导致无法捕获。

问题:逃逸出 Koa 的 co 异步调用链的代码,会导致 co 无法 catch 异常。

不如去看看egg怎么做的吧