ZIO 錯誤管理
ZIO 是 Scala 的框架,將錯誤區分為預期錯誤(Failures)、意外錯誤(Defects)跟 Fetal Error,並不是那麼容易分辨預期錯誤跟意外錯誤差在哪邊,本文解釋了使用時機。
預期錯誤
預期錯誤是我們預期在正常狀況下會發生的錯誤,所有的領域錯誤(domain error)、業務錯誤(business error)都是可預期錯誤。
訪問外部資料庫時,可能會遇到該資料庫短暫時間掛掉的狀況,因此我們會等個一兩秒,再重新訪問。但在重新嘗試幾次後,發現還是不行,我們便可能啟用備用方案,像是使用記憶體(in-memory)資料庫。
ZIO 將預期錯誤放到 Scala 的類別系統,有一個 E
錯誤類型參數,從而模擬應用程式的錯誤,讓開發者可以觀察到,並決定怎麼處理。
但 E
只包含可預期的錯誤,同一個 ZIO 的值仍然可能拋出異常(exception)。這些不可預期的狀況,被稱之為缺陷( defect)。
例如我們有一個 getUser
的函式,會從資料庫拿到一個 User
,假如沒有這個 User
,便會回傳 NoSuchElementError
錯誤,這對呼叫者來說,是可預期的錯誤。但是資料庫因為配置錯誤連線不到,這對應用開發者來說,是非預期的錯誤,所以並不需要顯示在 E
的型別中。
因此 getUser
的型別會長這樣:
ZIO[R, NoSuchElementError, A]
但這並非絕對,有時我們會想將缺陷(defect)定義成可預期的錯誤,以 E
表示,這樣可以讓編譯器在編譯時幫助我們掌握整個錯誤狀況。這有助於特定狀況下,處理領域錯誤(domain error)。
ZIO[R, NoSuchElement, , A]
不可預期錯誤
不可預期錯誤通常以缺陷(defects)表示,指的是我們無法在運行時處理這個問題。可能有必要關閉整個應用程序,以防止進一步的損壞。大多數意外錯誤都源於編程錯誤。
當我們使用新的數據模型升級我們的服務,而其他服務沒有使用新的數據合約升級,而是使用到過時的資料模型的服務時,如果我們沒有一個驗證階段,它們就會導致缺陷(defat)!
缺陷的另一個例子是內存錯誤,如緩衝區溢出、堆棧溢出、內存不足、無效訪問空指針等。大多數情況下,這些意外錯誤發生的原因是我們沒有編寫出內存安全和資源安全的程序,也可能是硬件問題或無法控制的外部問題。作為開發人員,我們不知道如何在運行時處理這類錯誤。我們應該進行調查,找到這些缺陷的確切根源。
由於我們無法處理意外錯誤,因此我們應該記錄這些錯誤及其相應的堆棧跟蹤(stack traces)跟上下文信息(context)。這樣以後我們就可以調查問題並嘗試修復它們。對於意外錯誤,我們能做的最好的辦法就是將其 "沙盒化",以限制其對整個應用程序造成的損害。例如,瀏覽器擴展中的意外錯誤不應該導致整個瀏覽器崩潰。
儘管缺陷發生時,我們沒有任何線索如何處理它,但在它 crash 我們的應用程式前,我們仍可做些事情,方便後續處理。像是記錄到 log aggreator、寄送電子郵件通知開發者、顯示更適合的"非預期錯誤"訊息給使用者等等。
但是 Typed Errors Guarantees 並不會將 defect 放進 Type Inference 中,所以一個說不會有 Failures 的 ZIO,不代表他不會 crash 或是中斷(interrupted)。
Typed errors don't guarantee the absence of defects and interruptions. Having an effect of type
ZIO[R, E, A]
, means it can fail because of some failure of typeE
, but it doesn't mean it can't die or be interrupted. So the error channel is only forfailure
errors.
ZIO 建議使用 Algebraic Data Type 來對領域(domain) 或子領域(sub domain)的錯誤建模。
Fetal Error
Fetal Error 指程式執行的 runtime 錯誤,像是 VirtualMachineError
跟 StackOverflow
,跟 Defects
又有不同。 Defects
通常是 ConfigError
、Data Serialization Error
(client side 跟 server side encoder/decoder的版本不匹配)。