本文的講述都是以 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。
博客原文在這裏