本期精读文章：[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. 引言

<img src="https://img.alicdn.com/imgextra/i2/O1CN01irhvv21FDyUJcJ0fe_!!6000000000454-2-tps-1960-720.png" alt="logo" width="500" />

错误处理无论对那种语言来说，都至关重要。在 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` 信息。不过这个解决方案也很成熟：

- 给应用内所需的 <script> 标签添加 crossorigin 属性；
- 在 js 所在的 cdn 服务器上添加 `Access-Control-Allow-Origin: *` HTTP 头；

# 4. 总结

Error 和 Stack 信息对于日常开发来说，尤为重要。如果可以将 Error 统计并上报，更有助于我们排查信息，发现在用户环境下到底触发了什么错误，帮助我们提升产品的稳定性。

> 讨论地址是：[JavaScript 中错误堆栈处理 · Issue #9 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/9)

> 如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，每周五发布。
