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。

博客原文在這裏

相關文章
相關標籤/搜索