Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中错误堆栈处理 #9

Closed
fanhc019 opened this issue Apr 24, 2017 · 9 comments
Closed

JavaScript 中错误堆栈处理 #9

fanhc019 opened this issue Apr 24, 2017 · 9 comments

Comments

@fanhc019
Copy link
Contributor

fanhc019 commented Apr 24, 2017

本期精读文章:http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html?utm_source=javascriptweekly&utm_medium=email

@ascoders ascoders mentioned this issue Apr 25, 2017
65 tasks
@ascoders
Copy link
Owner

ascoders commented Apr 26, 2017

这篇文章核心介绍了 captureStackTrace 方法的使用,通过截取有意义报错堆栈,并统计上报,有助于排查问题。

常用的断言库 chai 就是通过此方式屏蔽了库自身的调用栈,仅保留了用户代码的调用栈,这样用户会清晰的看到自己代码的调用栈。

关于 V8性能优化 里提到,只要 try..catch 分离到单独函数中,引擎就可以优化代码,所以不用顾虑性能问题了。

@jasonslyvia
Copy link
Contributor

Chai.js 的核心维护者通过介绍 Error.captureStackTrace 方法给我们揭露了 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就可以
  });
});

image

不难看出,power-assert 不仅能给出清晰的调用栈,更能告诉你具体断言的每个变量值是什么。

@fanhc019
Copy link
Contributor Author

中文版译文地址:https://zhuanlan.zhihu.com/p/25338849

@camsong
Copy link
Contributor

camsong commented Apr 27, 2017

这篇文章里提出了一些异常处理的技巧,重要的一点是 throw 时要使用 Error 对象。不要抛出非 Error 对象的值,如数字或字符串。因为这样无法知道抛出的类型就很难对错误进行统一处理。正确的做法应该是使用 throw new Error(“error message here”)。另外作者建议 Promise 的 reject 时也使用 Error 对象而不是字符串。

异常处理还有很多点需要考虑,这里有 Node.js 使用的异常处理方式 https://www.joyent.com/node-js/production/design/errors ,列出我感觉好的几点:

  • 区分操作异常和程序员的失误。操作异常指可预测的不可避免的异常,如无法连接服务器
  • 操作异常应该被处理。程序员的失误不需要处理,如果处理了反而会影响错误排查
  • 操作异常有两种处理方式:同步 (try…catch) 和异步(callback, event emitter)两种处理方式,但只能选择其中一种。
  • 函数定义时应该用文档写清楚参数类型,及可能会发生的合理的失败。以及错误是同步还是异步传给调用者的
  • 缺少参数或参数无效是程序员的错误,一旦发生就应该 throw
  • 传递错误时,使用标准的 Error 对象,并附件尽可能多的错误信息,可以使用标准的属性名

@fanhc019
Copy link
Contributor Author

fanhc019 commented Apr 27, 2017

文章开篇部分就讲解了 Stack Trace 和 Error 的基础。
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 对象,主要有两个重要属性 message 和 name 分别表示错误信息和错误名称。实际上,除了这两个属性还有一个未被标准化的 stack 属性,我们上面的代码也用到了 e.stack,这个属性包含了错误信息、错误名称以及错误栈信息。在 chrome 中测试打印出 e.stacke 类似。感兴趣的可以了解下 Sentry 的 stack traces,他集成了 TraceKit,会对 Error 对象进行规范化处理。

接下来当然就是捕获错误,这里作者也提到了 try...catch,它可以拿到出错的信息,堆栈,出错的文件、行号、列号等,但无法捕捉到语法错误,也没法去捕捉全局的异常事件。当然,也有一些异常监控的工具会把所有的 function 和文件块中加入 try...catch 来捕捉更多的静态信息。此外,在一些古老的浏览器下
try...catch 对 js 的性能也有一定的影响,而且对异步操作中的异常支持不好。

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

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

@ascoders
Copy link
Owner

ascoders commented Apr 27, 2017

对,补充一点,我也很反对 chai 的断言方式,过分语义化反而让代码更难读,有一种在使用 易语言
的感觉:

should(obj.arr[0]).be.equal(obj.number)

所以,我支持 avajs 的方式:

test.true(1 === count)
test.is(str, 'ok')

@ascoders
Copy link
Owner

ascoders commented Apr 28, 2017

@camsong 的观点补充一下,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(666) 注释起来,取消 throw Error(666) 的注释,会发现异常无法 catch 住。

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

@camsong
Copy link
Contributor

camsong commented Apr 28, 2017

reject 如果使用 Error 对象,会导致捕获不到错误的情况

@ascoders reject 使用 Error 对象指的是 reject(new Error('your message here'),并不是 throw。这样写比使用字符串优点是做统一错误处理时更加容易。使用 Promise 时要注意避免 try...catchthrow 的陷阱。

@ascoders
Copy link
Owner

专栏发表后请及时关闭

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants