精读《JavaScript 错误堆栈处理》
本期精读文章:JavaScript-Errors-and-Stack-Traces
1. 引言
错误处理无论对那种语言来说,都至关重要。在 JavaScript 中主要是通过 Error 对象和 Stack Traces 提供有价值的错误堆栈,帮助开发者调试。在服务端开发中,开发者可以将有价值错误信息打印到服务器日志中,而对于客户端而言就很难重现用户环境下的报错,我们团队一直在做一个错误监控的应用,在这里也和大家一起讨论下 js 异常监控的常规方式。
2. 内容概要
了解 Stack
Stack 部分主要在阐明 js 中函数调用栈的概念,它符合栈的基本特性『当调用时,压入栈顶。当它执行完毕时,被弹出栈』,简单看下面的代码:
function c() {
try {
var bar = baz;
throw new Error()
} catch (e) {
console.log(e.stack);
}
}
function b() {
c();
}
function a() {
b();
}
a();
上述代码中会在执行到 c 函数的时候跑错,调用栈为 a -> b -> c
,如下图所示:
很明显,错误堆栈可以帮助我们定位到报错的位置,在大型项目或者类库开发时,这很有意义。
认知 Error 对象
紧接着,原作者讲到了 Error 对象,主要有两个重要属性 message 和 name 分别表示错误信息和错误名称。实际上,除了这两个属性还有一个未被标准化的 stack 属性,我们上面的代码也用到了 e.stack
,这个属性包含了错误信息、错误名称以及错误栈信息。在 chrome 中测试打印出 e.stack
于 e
类似。感兴趣的可以了解下 Sentry 的 stack traces,它集成了 TraceKit,会对 Error 对象进行规范化处理。
如何使用堆栈追踪
该部分以 NodeJS 环境为例,讲解了 Error.captureStackTrace
,将 stack 信息作为属性存储在一个对象当中,同时可以过滤掉一些无用的堆栈信息。这样可以隐藏掉用户不需要了解的内部细节。作者也以 Chai 为例,内部使用该方法对代码的调用者屏蔽了不相关的实现细节。通过以 Assertion 对象为例,讲述了具体的内部实现,简单来说通过一个 addChainableMethod 链式调用工具方法,在运行一个 Assertion 时,将它设为标记,其后面的堆栈会被移除;如果 assertion 失败移除起后面所有内部堆栈;如果有内嵌 assertion,将当前 assertion 的方法放到 ssfi 中作为标记,移除后面堆栈帧;
3. 精读
参与本次精读的同学有:范洪春、黄子毅、杨森、camsong,该部分由他们的观点总结而出。
captureStackTrace 方法优劣
captureStackTrace 方法通过截取有意义报错堆栈,并统计上报,有助于排查问题。常用的断言库 chai 就是通过此方式屏蔽了库自身的调用栈,仅保留了用户代码的调用栈,这样用户会清晰的看到自己代码的调用栈。不过 Chai 的断言方式过分语义化,代码不易读。而实际上,现在有另外一款更黑科技的断言库正在崛起,那就是 power-assert。
直观的看一下 Chai.js 和 power-assert 的用法及反馈效果(以下代码及截图来自[小菜荔枝](http://www.jianshu.com/p/41ced3207a0c):
const assert = require('power-assert');
const should = require('should'); // 别忘记 npm install should
const obj = {
arr: [1,2,3],
number: 10
};
describe('should.js和power-assert的区别', () => {
it('使用should.js的情况', () => {
should(obj.arr[0]).be.equal(obj.number); // should api
});
it('使用power-assert的情况', () => {
assert(obj.arr[0] === obj.number); // 用assert就可以
});
});
抛 Error 对象的正确姿势
在我们日常开发中一定要抛出标准的 Error 对象。否则,无法知道抛出的类型,很难对错误进行统一处理。正确的做法应该是使用 throw new Error(“error message here”),这里还引用了 Node.js 中推荐的异常处理方式:
- 区分操作异常和程序员的失误。操作异常指可预测的不可避免的异常,如无法连接服务器
- 操作异常应该被处理。程序员的失误不需要处理,如果处理了反而会影响错误排查
- 操作异常有两种处理方式:同步 (try……catch) 和异步(callback, event - emitter)两种处理方式,但只能选择其中一种。
- 函数定义时应该用文档写清楚参数类型,及可能会发生的合理的失败。以及错误是同步还是异步传给调用者的
- 缺少参数或参数无效是程序员的错误,一旦发生就应该 throw。 传递错误时,使用标准的 Error 对象,并附件尽可能多的错误信息,可以使用标准的属性名
异步(Promise)环境下错误处理方式
在 Promise 内部使用 reject 方法来处理错误,而不要直接调用 throw Error
,这样你不会捕捉到任何的报错信息。
reject 如果使用 Error 对象,会导致捕获不到错误的情况,在我的博客中有讨论过这种情况:Callback Promise Generator Async-Await 和异常处理的演进,我们看以下代码:
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('我可以被捕获')
// throw Error('永远无法被捕获')
})
})
}
Promise.resolve(true).then((resolve, reject) => {
return thirdFunction()
}).catch(error => {
console.log('捕获异常', error) // 捕获异常 我可以被捕获
});
我们发现,在 macrotask 队列中,reject
行为是可以被 catch 到的,而此时 throw Error 就无法捕获异常,大家可以贴到浏览器运行试一试,第二次把 reject('我可以被捕获')
注释起来,取消 throw Error('永远无法被捕获')
的注释,会发现异常无法 catch 住。
这是因为 setTimeout 中 throw Error 无论如何都无法捕获到,而 reject 是 Promise 提供的关键字,自己当然可以 catch 住。
监控客户端 Error 报错
文中提到的 try...catch
可以拿到出错的信息,堆栈,出错的文件、行号、列号等,但无法捕捉到语法错误,也没法去捕捉全局的异常事件。此外,在一些古老的浏览器下 try...catch
对 js 的性能也有一定的影响。
这里,想提一下另一个捕捉异常的方法,即 window.onerror
,这也是我们在做错误监控中用到比较多的方案。它可以捕捉语法错误和运行时错误,并且拿到出错的信息,堆栈,出错的文件、行号、列号等。不过,由于是全局监测,就会统计到浏览器插件中的 js 异常。当然,还有一个问题就是浏览器跨域,页面和 js 代码在不同域上时,浏览器出于安全性的考虑,将异常内容隐藏,我们只能获取到一个简单的 Script Error
信息。不过这个解决方案也很成熟:
- 给应用内所需的
发表评论 (审核通过后显示评论):