JS 與 TS 的巢狀例外類別設計

JS 世界的例外處理一直處於相當混亂的狀態。下面是我認為的幾個原因:

任何東西都可以 throw

因為是弱型別,當初語言設計時並沒有規定只能throw Error type。因此你愛怎麼 throw 都是可以的。但 throw new Error() 的好處是,系統會幫你準備 stacktrace。

沒有標準的 Error

常見的 JS 的執行環境有 browser 和 nodejs,這些環境所定義的 Error 都有微妙的差別,而關於巢狀錯誤的設計 cause 近年來才納入標準。

看看 MDN 文件的 Error

  • message
  • name
  • cause (直到 chrome 93 版才出現)
  • fileName (非標準)
  • lineNumber (非標準)
  • columnNumber (非標準)
  • stack (非標準)

看看 NodeJS 的 Error

  • 沒有 name
  • cause (直到 v16.9.0 才加入)
  • code (和 MDN 不同,NodeJS 有相對穩定的 error code)
  • message (OK)
  • stack (OK)

例外處理的三個要求

這是我自己在設計例外處理類別時的三個要求。

  1. 能夠自訂錯誤類別
  2. 能夠顯示巢狀錯誤
  3. 能夠在例外發生時,一併放入當下環境的其他重要變數。

在 NodeJS 環境下,如果 runtime 不夠新,我會選擇安裝 nested-error-stacks 套件,他可以替我解決麻煩的 2。如果是 typescript 要再另外安裝 @types/nested-error-stacks,取得型別安全性。

使用上會像是這樣

nested-error-stack 解決的問題是,NestedError constructor 第一個參數放 message 描述錯誤,第二個參數放巢狀錯誤,而巢狀錯誤的描述會一起被放在最外層的 stack 裡面。

使用上會變成

可以看到 stack 清楚的串接了 nested error stack,這樣的錯誤訊息可以再被轉送到 logger 或是 bugsnag 等服務。

name() 的目的是覆寫掉原本 NestedError 的 name

context 的目的是當例外發生時,可以一併把例外的執行環境一起存下來,供 debug 時參考。型別訂成 {[key: string]: unknown} unknown 代表可以指派任意型別到 context 物件上,但是若是要讀取,必須先轉型,我想大多數的情況存下來的環境變數只用於 debug,unknown type 就夠用了。

相較於新版本的寫法

隨著 cause 屬性漸漸普及,接下來應該會看到越來越多這種寫法。

這種寫法的 stack 上只會有當下這個 Error 的 callstack,不會有 nestedError 的 callstack。

想要看更深的 stack 必須自行去 cause.stack 上面查找。我會比較喜歡一次把 stack 都放到最外層的寫法。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *