Error 常识
在 javascript 中,关于 Error,我们最熟悉的莫过于两类:
- 捕获异常
try { } catch (error) { }
- 抛出异常
throw new Error()
无法 try catch 的 Error
我之前对 Error 的印象也就到这里为止,直到出现了下面两个 case:
- 在异步操作中的异常
try { setTimeout(function () { throw new Error(); }, 0); } catch (e) { // won't be caught console.log(e); }
- 在 promise then 里的异常
try { Promise.resolve().then(function () { throw new Error(); }); } catch (e) { // won't be caught console.log(e); }
以上两个 throw error,都无法通过在外围 try catch 来捕获,其原因分别为:
- try catch 只能捕获在其中执行的同步代码所抛出的异常
- Promise 对 then 里回调函数都进行了异常捕获,只会继续向下一个 then 的第二参数 onReject 传递,而不会对外抛出。(也有一些 Promise 库会提供抛出 Uncaught Error 的功能)
不过 Promise 对异常的捕获,也仅限于同步代码的异常,所以 Promise 内向外抛出异常也可以使用简单的 setTimeout。但是要注意,外围的 try catch 依然无法捕获该错误。
try {
Promise.resolve().then(function () {
setTimeout(function () {
throw new Error();
});
});
} catch (e) {
// won't be caught
console.log(e)
}
如何捕获 Uncaught Error
那些没有在同步代码内 try catch 的异常,会产生如下效果
- 后续同步代码停止运行
- 在 NodeJS 下,直接退出运行 (可避免)
- 在浏览器下,之前设定的异步操作依然可以触发执行(包括timeout, dom 事件)
要捕获 Uncaught Error ,我们可以这么做
// 在 NodeJS 下,可以避免程序直接退出
process.on('uncaughtException', function (err) {
...
});
// 在浏览器中
window.onerror = function(msg, file, line, col, error) {
...
};
Stack Trace
每当异常发生,最重要的事情就是找到问题源头,这也是 Error 对象存在的价值。为什么这样说呢?有下面2点原因:
- throw 并不一定要接一个 Error,我们可以 throw 任何东西。
- 一个 Error 对象,不仅有 Error Message,更重要的是有函数调用栈 err.stack
// stack trace example
TypeError: lts.enableLongStackTrace is not a function
at Object.<anonymous> (/Users/kdepp/projects/me/lst/test/spec.js:3:5)
at Module._compile (module.js:435:26)
at Object.Module._extensions..js (module.js:442:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:311:12)
at Module.require (module.js:366:17)
at require (module.js:385:17)
需要注意的是,每个浏览器引擎对 Error.prototype.stack 的实现并不完全相同,且 stack 也还并没有一个 ecma 的标准。
本文后续,将以 V8 对 Error.prototype.stack 的实现来作为讨论依据。
### stack 格式
- 第一行,Error.toString()
- 后续多行,stack frame 信息,包含:
- 函数名
- 文件名
- 行数
- 列数
### stack 条目数量
可以通过设置 Error.stackTraceLimit 来控制 stack frame 的显示条数。
- 正数:即实际会显示的上限
- 负数:不显示任何 stack frame
- Infinity:显示所有 stack frame
### Error 上和 stack 有关的函数
关于 V8 的 stack,有三点需要注意:
- stack 的内容可以做定制化,生成实际 stack 文本的入口是 Error.prepareStackTrace,我们可以重写这个函数
- stack 的内容只有在被使用时,才会去调用 prepareStackTrace 渲染其内容
- stack 这个属性我们还可以武装到其他任意对象上
#### Error.prepareStackTrace(error, structuredStackTrace)
- **param**: error
- Error 对象
- **param**: structuredStackTrace
- 一个数组的 CallSite 对象,包含错误的函数名、行数等信息
// 简化的 prepareStackTrace 实现
Error.prepareStackTrace = function (error, structuredStackTrace) {
var trace = structuredStackTrace.map(function (callSite) {
return ' at: ' + callSite.getFunctionName() + ' ('
+ callSite.getFileName() + ':'
+ callSite.getLineNumber() + ':'
+ callSite.getColumnNumber() + ')';
});
return error.toString() + "\n" + trace.join("\n")
};
#### CallSite 对象 API
包含 getThis, getTypeName, getFunction, getFunctionName, getMethodName, getFileName, getLineNumber, getColumnNumber, getEvalOrigin, isTopLevel, isEval, isNative, isConstructor,具体含义可参考[V8 wiki](https://github.com/v8/v8/wiki/Stack%20Trace%20API#customizing-stack-traces)
#### Error.captureStackTrace(error, constructorOpt)
- **param**: error
- 希望被装上 stack 属性的任意对象
- **param**: constructorOpt
- 原 stack 中,从 constructorOpt 往上的 stack frame 都会被忽略,此参数可以省略
captureStackTrace 最大的作用就是让我们可以 throw 自己定制的 Error 类型,又不失 stack trace 信息。
function MyError(msg) {
this.msg = msg;
Error.captureStackTrace(this, MyError);
}
MyError.prototype.toString = function () {
return 'Oops, MyError: ' + this.msg;
};
throw new MyError('msg');
## Long Stack Trace
stack trace 也有短板,问题同样出在异步操作。正常的 stack trace 遇到异步回调就会丢失绑定回调前的 stack frame,来看个例子:
var foo = function () {
throw new Error('msg');
};
var bar = function () {
setTimeout(foo);
};
bar();
/*
Error: msg
at foo [as _onTimeout] (repl:2:7)
at Timer.listOnTimeout (timers.js:92:15)
*/
foo();
/*
Error: msg
at foo (repl:2:7)
at repl:1:1
at REPLServer.defaultEval (repl.js:164:27)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.<anonymous> (repl.js:393:12)
at emitOne (events.js:82:20)
at REPLServer.emit (events.js:169:7)
at REPLServer.Interface._onLine (readline.js:210:10)
at REPLServer.Interface._line (readline.js:549:8)
*/
在实际开发过程中,异步回调的例子数不胜数,尤其是在 NodeJS 环境下更是如此,如果不能知道异步回调之前的触发位置,会给 debug 带来很大的难度。这时,就出现了一个概念叫 long Stack Trace。
long Stack Trace 并不是 Javascript 原生就支持的东西,所以要拥有这样的 debug 功能,就需要我们做一些 hack,幸好在 V8 环境下,所有 hack 所需的 API,V8 都已经提供了。
思路是唯一的, 就是要在异步回调里,记录之前的 stack trace
### 针对异步回调
对于异步回调,需要做的就是在所有会产生异步操作的 API,都做一些手脚,这些 API 包括
* setTimeout, setInterval, setImmediate
* nextTick, nextDomainTick
* EventEmitter.addEventListener
* EventEmitter.on
* Ajax XHR
在这方面,做的比较的库可以参考:
* https://github.com/mattinsler/longjohn
* https://github.com/tlrobinson/long-stack-traces
### 针对 Promise
很多 Promise 库都包含了 longStackTrace 的功能,比如 bluebird。不过原生的 Promise 还不支持这个功能
### 打包解决方案
https://github.com/angular/zone.js
尚未细看 zone.js 的内容,不过自从 node 的 domain 模块被设为 deprecated, zone.js 好像就是异步异常捕获的最好选择,后面有时间,准备再细看一下 zone.js。