V8 的 Error 对象与栈追踪的妙用

本文的讲述都是以 Node.js 环境为例子,而 Node.js 使用的 JavaScript 引擎是 V8,所以理论上 Chrome 也能适用,其它浏览器我就不清楚了。javascript

现状

最近在写 Rize(欢迎 star) 的时候,一直为错误的栈追踪而愁。为何呢?这要从 Rize 的架构提及。java

因为 puppeteer 的绝大多数操做和 API 是异步的,而写异步代码的良好写法是用 ES2017 的 async/await 语法。git

但咱们都知道,async/await 实际上返回的是一个 Promise(即便你没有显式地 return 什么,它将是 Promise<void>)。很明显这样不能达到我想要的 API 链式调用的效果。我总不能对着 Promise 实例操做 prototype,而后把我本身的 API 挪上去吧?github

因此我使用了一个队列来保存用户想要进行的操做。也就是说,用户在调用 Rize 的 API 以后,并不会(也不可能)当即执行这些操做,而是放在队列中,等待时机适合(例如浏览器已经启动或者上一个操做已经完成)才执行。因为送入队列的是函数,所以在 push 的参数能够放心地使用 async/await数组

可是,一旦这些操做中出现错误,错误的定位变得十分麻烦。浏览器

下面这张图是直接用 Node.js 运行一个脚本的结果:架构

下面这张图是在 Jest 中执行一段代码的结果:异步

缘由是,async

首先,队列中的函数是 async function,这原本就给 debug 带来麻烦。函数

其次,这些函数并非当即在 API 中调用的,而是由专门的队列处理代码来调用。在错误发生时,V8 只能跟踪到那段队列处理代码那里。

这就为用户带来麻烦。错误发生了,却只能看着错误消息一点一点地去试着定位有问题的地方。

探索

为此我去阅读了 Node.js 的官方文档,看了 Errors 这一部分,不过彷佛没什么收获。

后来又找到了 TJ Holowaychuk 大神写的库 callsite,看看能不能有用。从文档上看,这个库并不适合个人需求。

但我阅读了 callsite 的源码,源码很短,十行不到。我在源码发现了一些信息。

callsite 是利用 V8 的 Stack Trace API 来获取函数调用处的一些信息,如文件名,行号等等。callsite 是如何获取这些数据的呢?

很是简单,就一句:

var err = new Error()
复制代码

对,仅仅是 new 一个 Error 实例,并且并非要抛出这个错误。

对比咱们平时的代码,一般当咱们 throw 一个错误以后,咱们能获得一些错误栈信息。但实际上,不须要 throw,仅仅是新建一个 Error 实例,也能让 V8 记录下当前的调用栈信息。

解决

既然发现这个事实,那咱们能够在须要记录调用栈的地方 new 一个 Error 实例。(千万不要把它抛出,否则你后面的代码就无法执行了)

此时当前的栈信息已经被记录下来,那么咱们怎样去使用这些信息呢?

若是用户的代码执行正常,那就没什么关系了。关键是在发生错误的时候。这里要提一提的是,个人那段队列处理代码是带有 try…catch 块的,大概长这样:

try {
  await fn()
} catch (error) {
  throw error
} finally {
  // do some stuff ...
}
复制代码

你可能好奇什么要把捕捉的异常还要抛出,由于我想要的是后面的 finally 块啊,但同时我又但愿异常能继续被抛出。

在这里,咱们就要对 catch 块作点功夫。固然这个 try…catch 块是可以获取到以前新建的 Error 实例的,在这里我省略了那部分代码。

为了方便叙述,我把以前 new 的那个 Error 实例命名为 trace,即假设 const trace = new Error()

显然把 trace 的全部栈信息都拿过来是不适合的,由于它有一些咱们并不须要的栈信息(这部分信息是位于 API 调用处以上的)。

每个 Error 实例都有个 stack 属性,它是一个多行字符串,咱们先把它的每行分开,保存在数组中:

const stack = trace.stack.split('\n')
复制代码

要注意 stack 的第一行不是栈信息,而是错误消息,这个不能去掉。因此:

stack.splice(1, 2)
复制代码

我这里有两行的信息是没用的,因此删去两行,实际上要根据你的须要修改第二个参数。

如今能够把 trace 的栈信息替换掉实际 error 的栈信息:

error.stack = stack.join('\n')
复制代码

结果

如今就能够获得友好的错误栈信息了:

配合 Jest 就能更好地定位问题所在之处:

最后是宣传一下我正在写的库 Rize(可让你简单优雅地使用 puppeteer),也就是本文提到的,欢迎前往 GitHub 并 star。

博客原文在这里

相关文章
相关标签/搜索