處理、傳播或中止錯誤(handling, propagating, or aborting on an error)
想像你帶隊登山,有人扭傷了腳踝。你有三個誠實的選項。你可以自己搞定——包紮好繼續走(處理)。你可以用無線電回報基地營,因為他們有擔架、也由他們決定要不要撤離(傳播)。又或者,如果這是個真正的緊急狀況、繼續下去對所有人都危險,你就立刻中止整趟健行(中止)。當一個程式遇到失敗時,它面對的正是這同樣三個選擇,而好的錯誤處理,泰半就在於每一次都選對其中之一。
處理意味著就在此地把錯誤解決掉,讓呼叫者甚至根本不必知道它發生過:從快取讀取失敗了,於是你改從較慢的來源取得;缺了一個選用的設定,於是你填入一個預設值;一次短暫的網路小故障,於是你重試並成功。呼叫者得到的是一次正常的成功。傳播意味著你無法、或不該在此地決定,於是你把失敗向上傳給呼叫者(回傳一個錯誤碼、回傳 NULL、回傳一個 Result),讓一個有更多脈絡的層去選——同時仍在退出途中清理你取得的任何資源。中止意味著情況壞到繼續下去並不安全,於是你讓程式停下:這是對臭蟲(一個被違反的不變式、一個不可能的狀態)的正確回應,那裡根本沒有合法的狀態可復原,做法是透過斷言失敗、abort()、或一次乾淨的致命錯誤退出。技藝在於讓回應與情況相配:對「你有足夠脈絡修好」的可復原錯誤就處理;對「必須由更高層決定」的可復原錯誤就傳播;對臭蟲與不可復原的損壞就中止。
為何重要:多數錯誤處理的失誤,其實都是選錯了回應。對一個可復原錯誤中止,會造就一個脆弱的程式,在它本該應付的第一個缺檔上就死掉。試圖處理一個臭蟲,意味著替一個你不理解的狀態寫復原碼,那通常只是把損壞藏起來、跛著繼續。默默吞掉一個錯誤——既不處理、不傳播、也不中止,就只是忽略它——是其中最糟的,因為失敗現在隱形了,日後會在離成因很遠之處再度浮現。誠實的說法:這三者是你對每一次失敗有意識做出的決定,而「什麼都不做」不是第四個選項——它是「沒有做決定」,也是多數健壯性失敗背後的臭蟲工廠。
對同一個回傳 NULL 的 fopen() 的三種回應:處理——退回內建預設值並繼續。傳播——回傳一個錯誤碼給呼叫者,由他決定。中止——如果這是整個程式必需的強制檔案,印出一個致命錯誤並以非零退出。哪一個正確,完全取決於這個檔案對程式意味著什麼。
同一個失敗,三種有效的回應;正確的那個取決於脈絡,絕不取決於習慣。
沒有第四個叫「忽略它」的選項。什麼都不做不是一種回應——它是「沒有做決定」,只是把失敗延後到離成因很遠的地方。對每一次失敗,有意識地在處理、傳播、中止之間做選擇。