本文的講述都是以 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。
部落格原文在這裡