资讯专栏INFORMATION COLUMN

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

Luosunce / 579人阅读

摘要:现状最近在写欢迎的时候,一直为错误的栈追踪而愁。由于送入队列的是函数,因此在的参数可以放心地使用。其次,这些函数并不是立即在中调用的,而是由专门的队列处理代码来调用。

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

现状

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

由于 puppeteer 的绝大多数操作和 API 是异步的,而写异步代码的良好写法是用 ES2017 的 async/await 语法。

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

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

但是,一旦这些操作中出现错误,错误的定位变得十分麻烦。

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

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

原因是,

首先,队列中的函数是 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("
")

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

stack.splice(1, 2)

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

现在可以把 trace 的栈信息替换掉实际 error 的栈信息:

error.stack = stack.join("
")
结果

现在就可以得到友好的错误栈信息了:

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

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

博客原文在这里

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93303.html

相关文章

  • JavaScript工作原理(一):引擎,运行时,调用堆栈

    摘要:调用栈是一种单线程编程语言,这意味着它只有一个调用栈。这就是调用栈的功能。简单代码示例当引擎执行这段代码时,调用栈为空,之后运行如下每个叫做堆栈帧。调用栈就是通过堆栈帧来追踪异常,堆栈帧基本就是调用栈出现异常时候的状态。 概述 几乎每个人都已经听说过V8引擎这个概念,而且大多人都知道JavaScript是单线程的,并且使用回调队列。 这篇文章中,我们将详细介绍这些概念,并解释JavaS...

    Jingbin_ 评论0 收藏0
  • 【前端进阶之路】内存基本知识

    摘要:在运行脚本时,需要显示的指定对象。大对象区每一个区域都是由一组内存页构成的。这里是唯一拥有执行权限的内存区。换句话说,是该对象被之后所能回收到内存的总和。一旦活跃对象已被移出,则在旧的半空间中剩下的任何死亡对象被丢弃。 内存管理 本文以V8为背景 对之前的文章进行重新编辑,内容做了很多的调整,使其具有逻辑更加紧凑,内容更加全面。 1. 基础概念 1.1 生命周期 不管什么程序语言,内存...

    Simon_Zhou 评论0 收藏0
  • JavaScript是如何工作:引擎,运行时间以及回调概述

    摘要:是如何工作的引擎,运行时以及调用栈的概述原文译者随着变得越来越流行,团队在多个层级都对它进行利用前端,后端,混合应用,嵌入式设备以及更多。这个将会在是如何工作的的第二部分进一步解释。 How JavaScript works: an overview of the engine, the runtime, and the call stack JavaScript是如何工作的:引擎,运...

    he_xd 评论0 收藏0
  • 【译】JavaScript 如何工作:对引擎、运行时、调用堆栈概述

    摘要:调用栈是一种数据结构,它记录了我们在程序中的位置。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。而且这不是唯一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会停止响应相当长一段时间。 原文地址: https://blog.sessionstack.com... PS: 好久没写东西了,最近一直在准备写一个自己的博客,最后一些技术方向已经敲定了,又可以...

    Warren 评论0 收藏0
  • JavaScript 工作原理之一-引擎,运行时,调用堆栈(译)

    摘要:本章会对语言引擎,运行时,调用栈做一个概述。调用栈只是一个单线程的编程语言,这意味着它只有一个调用栈。查看如下代码当引擎开始执行这段代码的时候,调用栈会被清空。之后,产生如下步骤调用栈中的每个入口被称为堆栈结构。 原文请查阅这里,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原...

    Betta 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<