精读JavaScript错误堆栈处理

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.stacke 类似。感兴趣的可以了解下 Sentry 的 stack traces,它集成了 TraceKit,会对 Error 对象进行规范化处理。

如何使用堆栈追踪

该部分以 NodeJS 环境为例,讲解了 Error.captureStackTrace,将 stack 信息作为属性存储在一个对象当中,同时可以过滤掉一些无用的堆栈信息。这样可以隐藏掉用户不需要了解的内部细节。作者也以 Chai 为例,内部使用该方法对代码的调用者屏蔽了不相关的实现细节。通过以 Assertion 对象为例,讲述了具体的内部实现,简单来说通过一个 addChainableMethod 链式调用工具方法,在运行一个 Assertion 时,将它设为标记,其后面的堆栈会被移除;如果 assertion 失败移除起后面所有内部堆栈;如果有内嵌 assertion,将当前 assertion 的方法放到 ssfi 中作为标记,移除后面堆栈帧;

3. 精读

captureStackTrace 方法优劣

captureStackTrace 方法通过截取有意义报错堆栈,并统计上报,有助于排查问题。常用的断言库 chai 就是通过此方式屏蔽了库自身的调用栈,仅保留了用户代码的调用栈,这样用户会清晰的看到自己代码的调用栈。不过 Chai 的断言方式过分语义化,代码不易读。而实际上,现在有另外一款更黑科技的断言库正在崛起,那就是 power-assert。

直观的看一下 Chai.js 和 power-assert 的用法及反馈效果:

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 方法来处理错误,而不要直接调用 throwError,这样你不会捕捉到任何的报错信息。

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('我可以被捕获') 注释起来,取消 throwError('永远无法被捕获') 的注释,会发现异常无法 catch 住。

这是因为 setTimeout 中 throw Error 无论如何都无法捕获到,而 reject 是 Promise 提供的关键字,自己当然可以 catch 住。

监控客户端 Error 报错

文中提到的 try...catch 可以拿到出错的信息,堆栈,出错的文件、行号、列号等,但无法捕捉到语法错误,也没法去捕捉全局的异常事件。此外,在一些古老的浏览器下 try...catch 对 js 的性能也有一定的影响。

这里,想提一下另一个捕捉异常的方法,即 window.onerror,这也是我们在做错误监控中用到比较多的方案。它可以捕捉语法错误和运行时错误,并且拿到出错的信息,堆栈,出错的文件、行号、列号等。不过,由于是全局监测,就会统计到浏览器插件中的 js 异常。当然,还有一个问题就是浏览器跨域,页面和 js 代码在不同域上时,浏览器出于安全性的考虑,将异常内容隐藏,我们只能获取到一个简单的 ScriptError 信息。不过这个解决方案也很成熟:

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

4. 总结

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

原文发布于微信公众号 - code秘密花园(code_mmhy)

原文发表时间:2019-02-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券