嗡嗡嗡~

Exception Handling

2018-04-05 (2018-04-09 updated)

前陣子寫 code 遇到 exception 或程式執行錯誤該丟出 exception 的情況,雖然知道 try catch 跟 throw exception,但「什麼時候」要「如何使用」exception 卻沒個概念然後搞得一團亂。只好來念念《例外處理設計的逆襲》,順便整理下嗑完書的簡略筆記。

區分 Fault、Error、Failure、Exception

首先區分幾個名詞的概念,不然這些東西都很像,不分清楚講到後來都不知道在講些什麼了。

Fault 的種類

依據「存在時間長短」,fault 可區分為:

依據「產生原因」,fault 可區分為:

Exception Handling vs Fault Tolerant

一個 software component 發生 error,必須加以處理以免變成 failure。error handling 可分為兩種:

因為程式語言並沒有特別區分兩者,所以實作上無論是 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 該怎麼處理?」的問題,算是個準則吧。

在正常功能尚未完成時不太容易決定 exception handling 該做在系統的哪一層,過早的例外處理實作不見得有用。甚至開發早期可能無法判斷該如何處理,要等到系統正常功能開發得差不多才有足夠多的資訊知道如何處理例外。一開始規定強健度等級在 1 或 2,開發後期有更充足的 context 後再決定是否要提升強健度等級。

強健度等級也是隨著軟體開發漸漸提升的,不需要一開始就要達到很高的強健度(畢竟一開始通常沒有那個時間)。

try、catch 以及 finally block 的責任

try block

catch block

finally block

實作的二三事

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 回復狀態可以簡化例外處理程式,並且分開狀態回復的實作跟正常功能的實作。


Blog comments powered by Disqus