当前位置: 首页 > news >正文

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

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

现状

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

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

但我们都知道,async/await 实际上返回的是一个 Promise(即使你没有显式地 return 什么,它将是 Promise<void>)。很明显这样不能达到我想要的 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('\n')
复制代码

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

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

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

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

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

结果

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

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

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

博客原文在这里

相关文章:

  • VR直播很火,但能取代传统电视直播吗?
  • Hibernate最全面试题
  • Python_网络编程
  • JDK1.7版本中的HashMap
  • 数字图像处理---直方图均衡化
  • Chef在InSpec 2.0增强了云安全的自动化功能
  • Kafka源码分析Consumer
  • cad提供的坐标转换
  • Java锁--Semaphore
  • Win8 Metro(C#)数字图像处理--2.43图像马赛克效果算法
  • PowerDesigner 概念数据模型(CDM)
  • [Contest20180313]灵大会议
  • Windows Developer Day - Adaptive Cards
  • 乘法逆元模板(Orz尧神)
  • 贪吃蛇
  • 4个实用的微服务测试策略
  • es的写入过程
  • fetch 从初识到应用
  • Javascript Math对象和Date对象常用方法详解
  • Javascript弹出层-初探
  • leetcode388. Longest Absolute File Path
  • Linux各目录及每个目录的详细介绍
  • PHP变量
  • ViewService——一种保证客户端与服务端同步的方法
  • Vue 重置组件到初始状态
  • 从tcpdump抓包看TCP/IP协议
  • 基于webpack 的 vue 多页架构
  • 软件开发学习的5大技巧,你知道吗?
  • 思考 CSS 架构
  • 算法-图和图算法
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • hi-nginx-1.3.4编译安装
  • Java数据解析之JSON
  • Spring Batch JSON 支持
  • 仓管云——企业云erp功能有哪些?
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • !$boo在php中什么意思,php前戏
  • # 达梦数据库知识点
  • (27)4.8 习题课
  • (ibm)Java 语言的 XPath API
  • (Note)C++中的继承方式
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (三)elasticsearch 源码之启动流程分析
  • (原创)攻击方式学习之(4) - 拒绝服务(DOS/DDOS/DRDOS)
  • .apk 成为历史!
  • .bat批处理(七):PC端从手机内复制文件到本地
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .NET Core WebAPI中使用swagger版本控制,添加注释
  • .net core控制台应用程序初识
  • .net framework 4.0中如何 输出 form 的name属性。
  • .Net Winform开发笔记(一)
  • .NET 程序如何获取图片的宽高(框架自带多种方法的不同性能)
  • .NET 的程序集加载上下文