Exception Handling
前陣子寫 code 遇到 exception 或程式執行錯誤該丟出 exception 的情況,雖然知道 try catch 跟 throw exception,但「什麼時候」要「如何使用」exception 卻沒個概念然後搞得一團亂。只好來念念《例外處理設計的逆襲》,順便整理下嗑完書的簡略筆記。
區分 Fault、Error、Failure、Exception
首先區分幾個名詞的概念,不然這些東西都很像,不分清楚講到後來都不知道在講些什麼了。
- (從外部看)一個 component 沒有提供正確服務稱為 service failure,簡稱 failure。
- error 是種「狀態」,表示 component 內部處於錯誤狀態。這種狀態可能導致 component 執行失敗,造成 failure。
- fault 則是導致 error 發生的「原因」。
- 程式語言以 exception 表達 error 與 failure 的概念。
- 從不同角度來看(如 caller 跟 callee),exception 可以代表 error 也可以代表 failure。
- 遇到某種情況要不要丟出 exception,決定方式之一是看在該 function 的語意及功能下,該狀況是不是屬於「目前可能處在不正確的狀態」或「被 call 的 function 沒做好該做的事」。如果是,當然就丟出 exception。
Fault 的種類
依據「存在時間長短」,fault 可區分為:
- transient fault
出現一次就消失,重新執行一次操作又能正常執行。 - intermittent fault
出現後消失,接著重複出現。例如 memory leak,重新啟動後能正常使用,但過陣子 memory 又爆掉。 - permanent fault
發生後除非將有問題的 component 換掉或修好,否則 component 會一直處於 failure 狀態,如 bug。
依據「產生原因」,fault 可區分為:
- design fault
就是 bug,如誤解需求、邏輯寫錯等等。design fault 屬於 permanent fault,除非 deveoper 修好,不然不會自己消失的啦~(我也希望有一天 bug 會自己修好) - component fault
硬體 component failure 產生的 error,或者 software component 與 software component、software component 與執行環境、software component 與硬體環境,彼此互動時所產生的不正常情況。component fault 有可能是 transient fault、intermittent fault 或 permanent fault,因此有不同的處理方式。針對 transient fault 只要 retry 系統就能恢復正常,例如 call REST API 時網路抖了一下連不上。但 intermittent 與 permanent fault 得釐清 root cause,單純的 retry 無法解決問題。
Exception Handling vs Fault Tolerant
一個 software component 發生 error,必須加以處理以免變成 failure。error handling 可分為兩種:
- exception handling:負責處理 component fault(可預期的錯誤狀況)。
- fault-tolerant programming:負責處理 design fault(不可預期的錯誤狀況)。
因為程式語言並沒有特別區分兩者,所以實作上無論是 exception handling 還是 fault-tolerent programming 都使用程式語言的 exception handling 機制來處理。
exception handling 跟 fault-tolerant programming 的成本差很多。如果要做到 fault-tolerant,像 Java 的 NullPointerException
會變成得在程式中處理的 exception,但這在大多數時候(尤其是內部 function 間的呼叫)算是 design fault,而且會出現 NullPointerException
的地方很多,所有地方都要處理當然會提升開發成本。
fault-tolerant,容錯,顧名思義是「能容忍程式裡的錯誤,就算是程式有錯也要能夠正常執行」。fault-tolerant 設計通常用在需要非常 robust 的應用系統,像飛機啊軍事啊等等。(沒弄好會死人的系統)
區分 exception handling 跟 fault-tolerant programming 並且確認開發的軟體系統需要多強的 robustness、能付出多少成本,就能知道在實作上需要處理哪些 exception,才不會一下這邊處理了屬於 design fault 的 exception 一下那邊又不處理一團亂。
定義強健度等級
例外處理設計的第一步是定義強健度等級。強健度等級是例外處理的需求,有了需求開發時就知道要如何處理 exception。
等級 0:未定義(undefined)
沒特別定義跟做什麼的時候。
簡單說,一切都是混亂不清。caller 無法確定 service 是否有完成任務。發生問題時 caller 可能知道也可能不知道、service 的狀態是不明或者錯誤狀態。發生問題後 service 可能終止也可能繼續跑。
等級 1:錯誤回報(Error-reporting)
service 發生錯誤時一定要讓 caller 知道。caller 能確切知道 service 是否有成功達成任務。發生問題時 service 的狀態可能是不明或錯誤狀態。發生問題後 service 必須終止執行,因為狀態已經不明,繼續執行可能會造成更多錯誤。
要達到這個等級就是把 exception 都往外丟,然後在主程式捕捉所有 exception 並回報給 user 或 developer 知道。
等級 2:狀態回復(State-recovery)
又稱 weakly tolerant。
有 error-reporting 的要求,並且錯誤發生後 service 須保證系統依然處在正確狀態。因為狀態依然正確,所以發生問題後系統仍然可以繼續執行。
要達到此等級,要多做 error handling 以及 cleanup,讓系統回復到正確狀態並且釋放資源,例如把修改過的 db 做 rollback 以及釋放要來的 memory。
等級 3:行為回復(Behavior-recovery)
又稱 strongly tolerant。
service failure 時會試圖排除困難,達成原本應該做到的任務。
除了狀態回復需要做的事情外,行為回復還要想替代方案達成原本的任務。為了不讓下次執行任務依然失敗,會先啟動 fault handling 排除造成失敗的原因,再套用 retry 等技巧來嘗試繼續提供服務。
目前遇到的都沒有那麼複雜,往往是對 transient fault 直接 retry,例如連不上對方 server 就等個幾秒再試一次。
無論如何都無法回復行為?
將強健度等級降為 2。
如果行為回復失敗,service 要嘗試達到狀態回復,如果依然失敗,再降到錯誤回報等級。
如何使用強健度等級?
這回答了我「在程式中遇到 exception 該怎麼處理?」的問題,算是個準則吧。
- 預設所有 function 要達到強健度等級 1。
- 系統開發到一定程度才較容易知道 exception 該在哪層處理以及如何處理、哪些 function 需要提升強健度。要求 function 都達到錯誤回報可以在開發時將問題曝露出來,讓 developer 比較容易 debug、決定如何處理 exception。
- 要求 function 達到錯誤回報,能讓 developer 在開發正常流程的階段知道遇到 exception 該怎麼處理(往外丟就對了),減少各自用各自方式處理的混亂。
(別說每個人處理方式不同會亂,我自己寫沒想好都會一團亂了啊哈哈) - 實作上只要在主程式統一捕捉 exception 就不會讓程式不預期的當掉,也可以統一做 logging,其他 function 只要安心丟出 exception 即可。
- 針對特定 function 要求達到強健度等級 2。
- 例如資料庫處理。
- 除非特別要求或者會很難用,否則不特別要求做到行為回復。
- 像在 call REST 之類 API 會做 retry,retry 一定次數依然失敗後才當作 failure。
在正常功能尚未完成時不太容易決定 exception handling 該做在系統的哪一層,過早的例外處理實作不見得有用。甚至開發早期可能無法判斷該如何處理,要等到系統正常功能開發得差不多才有足夠多的資訊知道如何處理例外。一開始規定強健度等級在 1 或 2,開發後期有更充足的 context 後再決定是否要提升強健度等級。
強健度等級也是隨著軟體開發漸漸提升的,不需要一開始就要達到很高的強健度(畢竟一開始通常沒有那個時間)。
try、catch 以及 finally block 的責任
try block
- 實作 function 的功能,包含主要方法與替代方法。
- 為恢復狀態做準備,例如建立 checkpoint。
- 回報系統錯誤。如果發生錯誤導致無法提供正確服務,而且該錯誤無法在 function 內部處理,則可以在此直接回報服務 failure(丟出 exception)。
catch block
- 進行 error handling 與 fault handling。
- error handling 是如果系統因為發生問題而處於 error 狀態,需要修正這種 error 狀態,讓系統回復到可以繼續運行的正常狀態。
- fault handling 則是要嘗試排除造成錯誤的根本原因。要程式來做這件事頗難,所以通常交由人來處理。
- 回報錯誤狀況。
- failure 可以在 try block 也可以在 catch block 回報,看是在哪裡發生。
- 回報的方式有丟出 exception、在螢幕上印出錯誤訊息、寫到 log 等等。
- 控制 retry 流程。
- 如果 exception 發生後要以 retry 的方式處理,可以在 catch block 控制 retry 流程。
- 舉例來說,在 catch block 檢查 retry 次數,超過某個次數就算失敗、丟出 exception。
finally block
- 釋放資源並且回報釋放資源失敗的狀況。
- 如果在 try block 做了 checkpoint,在這裡要把 checkpoint 砍掉免得佔用 resource。
實作的二三事
exception handling 的基本做法就是以語意清楚的 exception 回報所有發生的問題,之後再針對需要更 robust 的部份去做狀態或行為回復。
前面多是概念,總要講點實作面,但我又不想一一列舉各種實作小技巧,簡單寫點二三事就好。
越底層越不特別處理 exception
通常寫某個 module 是因為它現在會在某種應用情境下使用到,但越底層的 module 就越不要針對目前的應用情境去處理問題或 exception,只要往上丟 exception 就好。底層 module 針對特定情境處理 exception,會讓 module 難以在其他情境中使用,降低 module 的 reusable。
用不同 exception class 區分不同 fault
主要是區分 design fault 與 component fault。
以不同的 exception class 區分兩種 fault 後,開發時就能依照需要 exception handling 還是 fault-tolerant 知道是否需要處理特定 exception。debug 時也比較容易知道發生什麼問題,是有機會回復的問題還是是 bug。
對於丟出 exception 的 module,也可以藉由丟出不同的 exception 區分程式中的 fault 是哪種。
我想分清楚不同 fault 對實作跟 debug 是有好處的。
error-reporting + catch exception in main function
在達到強健度等級 1 error reporting 時,function 們就是丟出 exception。exception 一直往上傳,為了避免程式不預期結束,可以在 main function 或 thread entry point 或程式的各 entry point 加上 try catch,在 catch block 捕捉所有的 exception 並且寫入 log。這樣 logging 的動作只在最外層做一次,內部 function 不需要特別再管 logging。
不要在 console 印 exception
「遇到 exception 不知道怎麼處理,就把它印出來就好啦!這樣就有處裡啦!」
問題是,印在 console 的東西很難被注意到,所以 exception 只印在 console 跟被忽略差不了多少,甚至還有已經處理 exception 的錯覺。
因為現在程式多是圖形介面,印在 console 不只使用者不會看到,連 developer 都可能因為 console 太多東西而忽略 exception。
用哪裡出了問題來命名 exception
用哪裡出了問題、出了什麼問題來命名,而不是用誰丟出 exception。
關於 exception object 所帶的資訊
從 exception 的名稱、其繼承結構、身上帶的其他 exception、error code 以及訊息等等可以知道發生的問題的資訊,能在 exception 上越清楚的附上這些資訊,越有利於 error handling 以及 debug。
將低階層 exception 轉成高階層所理解的 exception
不要將低階層的 implementation exception 直接傳給上層的人,因為這樣收到 exception 的人會被底層的實作細節綁住,造成不必要的相依性。
exception handling 失敗怎麼辦?
簡單說,丟出一個代表「例外處理失敗」的例外。將「代表例外處理失敗的例外」附在原本 exception 上,再丟出原本的 exception。
用 checkpoint class 做狀態回復
以 checkpoint class 負責狀態回復相關的動作,這個 class 具有保存狀態、回復狀態以及丟棄狀態等功能。
在 try block 改變系統狀態前保存狀態,發生 exception 後在 catch block 回復狀態,最後無論有無發生 exception 都在 finally block 丟棄先前保存的狀態。
保存及回復狀態的實作會因功能不同而不同。使用 checkpoint 回復狀態可以簡化例外處理程式,並且分開狀態回復的實作跟正常功能的實作。