本期精读文章:[JavaScript-Errors-and-Stack-Traces](http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html?utm_source=javascriptweekly&utm_medium=email) [中文版译文](https://zhuanlan.zhihu.com/p/25338849) # 1. 引言 logo 错误处理无论对那种语言来说,都至关重要。在 JavaScript 中主要是通过 Error 对象和 Stack Traces 提供有价值的错误堆栈,帮助开发者调试。在服务端开发中,开发者可以将有价值错误信息打印到服务器日志中,而对于客户端而言就很难重现用户环境下的报错,我们团队一直在做一个错误监控的应用,在这里也和大家一起讨论下 js 异常监控的常规方式。 # 2. 内容概要 ## 了解 Stack Stack 部分主要在阐明 js 中函数调用栈的概念,它符合栈的基本特性『当调用时,压入栈顶。当它执行完毕时,被弹出栈』,简单看下面的代码: ```plain 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`,如下图所示: ![](https://img.alicdn.com/tfs/TB1hqekQVXXXXa1XVXXXXXXXXXX-734-256.png) 很明显,错误堆栈可以帮助我们定位到报错的位置,在大型项目或者类库开发时,这很有意义。 ## 认知 Error 对象 紧接着,原作者讲到了 Error 对象,主要有两个重要属性 message 和 name 分别表示错误信息和错误名称。实际上,除了这两个属性还有一个未被标准化的 stack 属性,我们上面的代码也用到了 `e.stack`,这个属性包含了错误信息、错误名称以及错误栈信息。在 chrome 中测试打印出 `e.stack` 于 `e` 类似。感兴趣的可以了解下 Sentry 的 [stack traces](https://sentry.io/features/stacktrace/),它集成了 TraceKit,会对 Error 对象进行规范化处理。 ## 如何使用堆栈追踪 该部分以 NodeJS 环境为例,讲解了 `Error.captureStackTrace`,将 stack 信息作为属性存储在一个对象当中,同时可以过滤掉一些无用的堆栈信息。这样可以隐藏掉用户不需要了解的内部细节。作者也以 Chai 为例,内部使用该方法对代码的调用者屏蔽了不相关的实现细节。通过以 Assertion 对象为例,讲述了具体的内部实现,简单来说通过一个 addChainableMethod 链式调用工具方法,在运行一个 Assertion 时,将它设为标记,其后面的堆栈会被移除;如果 assertion 失败移除起后面所有内部堆栈;如果有内嵌 assertion,将当前 assertion 的方法放到 ssfi 中作为标记,移除后面堆栈帧; # 3. 精读 参与本次精读的同学有:[范洪春](https://www.zhihu.com/people/fanhc/activities)、[黄子毅](https://www.zhihu.com/people/huang-zi-yi-83/answers)、[杨森](https://www.zhihu.com/people/yangsen/answers)、[camsong](https://www.zhihu.com/people/camsong/answers),该部分由他们的观点总结而出。 ## captureStackTrace 方法优劣 captureStackTrace 方法通过截取有意义报错堆栈,并统计上报,有助于排查问题。常用的断言库 chai 就是通过此方式屏蔽了库自身的调用栈,仅保留了用户代码的调用栈,这样用户会清晰的看到自己代码的调用栈。不过 Chai 的断言方式过分语义化,代码不易读。而实际上,现在有另外一款更黑科技的断言库正在崛起,那就是 [power-assert](https://github.com/power-assert-js/power-assert)。 直观的看一下 Chai.js 和 power-assert 的用法及反馈效果(以下代码及截图来自[小菜荔枝](http://www.jianshu.com/p/41ced3207a0c): ```js 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就可以 }); }); ``` ![](https://cloud.githubusercontent.com/assets/1336484/25432441/0696cda2-2ab7-11e7-94a7-6719acdcb7af.png) ## 抛 Error 对象的正确姿势 在我们日常开发中一定要抛出标准的 Error 对象。否则,无法知道抛出的类型,很难对错误进行统一处理。正确的做法应该是使用 throw new Error(“error message here”),这里还引用了 Node.js 中推荐的异常[处理方式](https://www.joyent.com/node-js/production/design/errors): - 区分操作异常和程序员的失误。操作异常指可预测的不可避免的异常,如无法连接服务器 - 操作异常应该被处理。程序员的失误不需要处理,如果处理了反而会影响错误排查 - 操作异常有两种处理方式:同步 (try……catch) 和异步(callback, event - emitter)两种处理方式,但只能选择其中一种。 - 函数定义时应该用文档写清楚参数类型,及可能会发生的合理的失败。以及错误是同步还是异步传给调用者的 - 缺少参数或参数无效是程序员的错误,一旦发生就应该 throw。 传递错误时,使用标准的 Error 对象,并附件尽可能多的错误信息,可以使用标准的属性名 ## 异步(Promise)环境下错误处理方式 在 Promise 内部使用 reject 方法来处理错误,而不要直接调用 `throw Error`,这样你不会捕捉到任何的报错信息。 reject 如果使用 Error 对象,会导致捕获不到错误的情况,在我的博客中有讨论过这种情况:Callback Promise Generator Async-Await 和异常处理的演进,我们看以下代码: ```js 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` 信息。不过这个解决方案也很成熟: - 给应用内所需的