發生非預期例外時的 checklist - 要讓應用結束還是繼續,先看的判斷表

· · Windows 開發, 例外處理, 設計, C# / .NET, 可靠性

下載日英雙語 Excel 檢核表

只要談到非預期例外,很容易就直接進入「是要讓它掛,還是 catch 繼續」的二選一。
但實務上,這種二選一的擺法其實有點粗糙。

真正要看的,是 能不能把可能已經毀損的範圍封住

  • 只讓那一次操作失敗就好了嗎
  • 重新初始化那個畫面/連線/worker 就好了嗎
  • 是不是整個行程的一致性已經不可信

按這個順序看,會整理得多。

本文以 C# / .NET 的 Windows 應用、常駐應用、Windows 服務、設備整合工具等為前提,把 發生非預期例外時可以繼續的條件,與應該結束的條件 整理成一張判斷表。

1. 先下結論

  • catch (Exception) 吃掉繼續跑,大多是危險的。
  • 可以繼續的前提是 3 項同時成立:能丟掉失敗的單位能還原共用狀態能說明外部副作用
  • UI 的 1 次操作、1 筆輸入、1 個 job,只要處理邊界明確,就可能可以繼續。
  • 反之,若牽涉到共用可寫狀態、常駐迴圈、主執行緒、啟動流程、原生邊界、有記憶體毀損氣味,就偏向結束。
  • StackOverflowExceptionAccessViolationExceptionOutOfMemoryException 這類懷疑「整個行程健康度」的例外,不要以繼續為前提。
  • WPF 或 Windows Forms 雖然有捕捉未處理例外後還能繼續運作的路,但 能繼續繼續也安全 是兩件事。
  • 長時間執行的服務或監控應用,與其帶著半毀的狀態活著,不如倒下後被重啟;這樣反而更安全、也更好診斷。

簡單說,判斷的主軸不是「能不能 catch」,而是 能不能恢復不變條件

2. 本文說的「非預期例外」

2.1 預期與非預期要分

首先,罕見的例外非預期的例外並不一樣。

例如下面這些,就算頻率低,也可以當成 預期之內

  • 使用者選到不存在的檔
  • 連線暫時 timeout
  • 匯入的 CSV 某行壞掉
  • 取消時丟出 OperationCanceledException
  • 業務規則違反,只讓這次處理失敗

這些的共通點是 可以在設計時決定要怎麼處理這個失敗

而本文主要要談的 非預期例外,例如:

  • 自家程式碼的前提崩掉,丟出 NullReferenceExceptionInvalidOperationException
  • 在更新共用狀態的途中丟例外,不知道改到哪
  • 監控迴圈或訊息處理的父迴圈掛了
  • COM / P/Invoke / 廠商 SDK 邊界出現異常
  • 出現 AccessViolationExceptionStackOverflowException 這類「整個行程的健康檢查已經紅燈」的狀況

也就是說,「這個例外之後,還能不能信任應用程式的狀態」已經不明

2.2 看似二選一,其實是三選一

讓這個議題變亂的元兇,是把「繼續」當成一種來想。

實務上大致是 3 段:

選擇 意義
只讓這次操作失敗然後繼續 保留畫面,只把這次存檔或匯入當失敗
只停掉子系統再繼續 重新初始化連線、畫面、worker、子行程
結束整個行程 狀態毀損範圍讀不出來,改用重啟策略

即使都叫「繼續」,裝沒事直接跑下去把壞的部分切出去再繼續 完全不是一件事。

3. 先看的判斷表

3.1 全貌

先看這張表,大致方針就定下了。

狀況 首選 理由
只有 1 筆輸入、1 個畫面操作、1 個 job 失敗,狀態可丟 偏繼續 能把失敗單位封住
例外後可以把物件或連線丟掉重建 偏重新初始化子系統 能將毀損範圍局部化
共用狀態改到一半,但不知改到哪 偏結束 不變條件可能被打破
DB/檔案/裝置指令等外部副作用半途,無法說明重複或未反映 偏結束 與外部世界的一致性不明
監控迴圈、重連迴圈、訊息處理的父迴圈因非預期例外掛了 偏結束 靜悄悄只死一部分功能,會變 zombie
啟動流程、設定載入、DI 組裝、必要相依初始化失敗 以啟動失敗結束 半吊子啟動更危險
AccessViolationExceptionStackOverflowException、嚴重的 OutOfMemoryException、原生側有毀損氣味 立即結束 整個行程健康度存疑
危險處理被隔離在另一個行程,主行程沒事 主行程繼續,重啟子行程 故障領域已分離
flowchart TD
    A["非預期例外"] --> B{"記憶體毀損 / 堆疊枯竭 / 致命資源枯竭的氣味?"}
    B -- "是" --> Z["結束 / FailFast / 重啟"]
    B -- "否" --> C{"失敗的單位能丟棄嗎?"}
    C -- "否" --> Y["偏結束"]
    C -- "是" --> D{"共用狀態能 rollback / 重新初始化嗎?"}
    D -- "否" --> X["停子系統 or 結束"]
    D -- "是" --> E{"外部副作用可被說明嗎?"}
    E -- "否" --> X
    E -- "是" --> W["只把這次操作當作失敗,繼續"]

3.2 比例外型別更要先看的事

不要只靠例外型別立刻下定論。
先看的是下列這些:

觀察點 要確認什麼
在哪裡發生 UI 事件、單筆 job、父迴圈、啟動流程、原生邊界
處理走到哪 中途有沒有改到記憶體狀態、DB、檔案、裝置狀態
可能毀損的範圍 只有該物件、整個畫面、還是整個行程
是否可 rollback 能否丟掉重建、能否用 transaction 復原
外部副作用 已送出/未送出、重複執行是否安全、能否補償
監控/重啟 掛掉後有無自動重啟或恢復路徑

3.3 高風險例外

沒必要把所有例外型別都講一遍,但下列幾種不適合以「繼續」為前提:

例外/徵兆 首選 觀察理由
StackOverflowException 立即結束 呼叫堆疊已經壞掉,很難假定能正常恢復
AccessViolationException 立即結束 被保護記憶體被非法存取,疑似原生邊界或記憶體毀損
OutOfMemoryException 偏結束 恢復流程本身仍要額外分配,反而容易再炸
非預期的 NullReferenceException / InvalidOperationException 視上下文,但偏結束 自家前提崩掉,中途變更可能仍殘留
從父迴圈漏出的非預期例外 偏結束 核心功能死掉,行程卻還留著,很危險
從 COM / P/Invoke / 廠商 SDK callback 起的異常 立即結束至偏結束 只看 managed 端無法判斷安全性

4. 依「發生地點」判斷

4.1 UI 事件

按鈕點擊、畫面切換、搜尋、開檔等 UI 事件,可以繼續的空間比較大。
但有條件。

比較容易繼續的情境:

  • 在讀入之前就失敗,還沒動到業務狀態
  • 只有對話框內的暫存狀態壞了,關了就沒了
  • 例外後可以重建 ViewModel 或連線
  • 可以誠實告訴使用者「這次操作失敗了」

反之,偏結束的情境:

  • 畫面與 domain 狀態都改到一半
  • 動到 static/singleton/快取等其他畫面也在看的共用狀態
  • 例外後按鈕啟用或選取狀態殘留,整體一致性不明
  • UI thread 上發生非預期例外,畫面或通知處理到哪不清楚

4.2 逐筆處理的 job / request

這種邊界很容易繼續。

  • 1 則訊息
  • 1 個檔案
  • 1 個 HTTP 請求
  • 1 個匯入 job
  • 1 個批次目標

只要單位明確,就能 只讓那 1 筆失敗,繼續下一筆。

但需要具備:

  • 從外部看得出失敗單位
  • 中途變更可用 transaction 或補償復原
  • 同一處理再執行一次結果不會壞
  • 能把失敗送到隔離佇列或錯誤紀錄

4.3 常駐迴圈/監控/佇列處理

這是隨便繼續最容易出事的地方。

例如:

  • 重連迴圈
  • 監控迴圈
  • 佇列消費迴圈
  • 定期輪詢
  • 設備狀態監控
  • 工作列常駐處理

這類處理最可怕的是 父迴圈因一次非預期例外死掉,但行程還活著

這裡策略要分:

  • 每筆處理的邊界 捕捉預期內例外
  • 父迴圈 漏出非預期例外時,偏向結束行程

4.4 啟動流程

把啟動失敗擺在「先啟動了再說」,幾乎一定會後悔。

  • 讀不到必要設定
  • 版本升級/migration 失敗
  • 缺必要資料夾或憑證
  • 核心服務初始化失敗
  • 相依組裝壞掉

這種情境以 啟動失敗結束 最乾淨。

4.5 原生邊界/COM/P/Invoke/unsafe

這塊要另行嚴格一點:

  • COM
  • P/Invoke
  • C++/CLI 外側
  • 廠商 SDK
  • 由 callback 回到 native 那側的程式碼
  • unsafe 的處理

下列情境偏結束:

  • AccessViolationException
  • 疑似 heap 毀損、double free
  • handle 異常、use-after-free 氣味
  • callback 邊界突然死掉

5. 可以繼續的條件

下列條件大致齊備時,可以繼續:

條件 意義
失敗單位明確 1 操作、1 畫面、1 job、1 連線等,清楚哪一塊要丟
能丟棄狀態 能重建或視為未反映
共用狀態受保護 汙染不會蔓延到其他功能
能說明外部副作用 送了/沒送/可重送,能交代清楚
能誠實告知使用者 可以顯示「這次處理失敗」
可觀測 日誌、指標、dump 後續可追

6. 應該結束的條件

反之,符合下列情況就偏結束:

  • 不知道中途改了什麼
  • 動到了共用可寫狀態,一致性讀不出來
  • 鎖、佇列、執行緒、監控迴圈的生命週期壞了
  • 外部副作用的重複/遺漏/半途無法交代
  • 啟動流程或核心基礎設施的初始化失敗
  • 懷疑原生邊界或記憶體毀損

到這個等級,與其鑽研 如何漂亮地繼續,不如改為 倒下後方便恢復 的設計。

7. 依典型情境的建議

情境 建議 理由
開檔按鈕指定不存在的路徑 只讓那次操作失敗,繼續 狀態毀損局部
CSV 匯入只有 1 行壞了 1 行或 1 檔失敗,繼續 失敗單位可封住
存檔中途丟出非預期 NullReferenceException 重建畫面至偏結束 不確定 ViewModel/業務狀態改到哪
佇列的某筆訊息違反業務規則 只讓那筆失敗,繼續 可以丟到隔離佇列
佇列消費的父迴圈被非預期例外打掛 偏結束行程 worker 整體生命週期已壞
啟動時必要設定讀不到 以啟動失敗結束 半吊子啟動風險更大
廠商 SDK callback 附近丟 AccessViolationException 立即結束 不能無視記憶體毀損可能性
非核心的 telemetry 發送失敗 只停用該功能,繼續 主功能與故障領域可分

8. 常見的 NG

8.1 catch (Exception) 只印 log 就繼續

這相當危險。
既掩蓋原因,又讓毀損狀態被延命。

8.2 想在最後的未處理例外 handler 裡做恢復

AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledException 等等,用來 最後記錄 很有用,但 不是神奇的復活點

8.3 有外部副作用還輕率 retry

裝置指令、寄信、計費、搬檔、更新 DB 等,沒有先確保同一處理可以安全重複執行就 retry,會把重複執行事故變成主角。

8.4 監控迴圈死了,UI 還留著

只剩外觀活著、實際沒在做事的應用程式,會給周圍帶來很大麻煩。

8.5 沒設計「倒下」卻堅持「不想倒下」

不想倒下,就得先做這些:

  • 自動重啟
  • session 還原
  • 中途成果的儲存
  • 重新執行的安全性
  • 故障領域的分離

9. 實作上的整理重點

9.1 把 catch 擺在邊界

與其在深層到處 catch,不如在下列 可定義失敗單位的地方 接例外:

  • UI 操作邊界
  • 1 個 request 邊界
  • 1 個 job 邊界
  • 1 個連線邊界
  • 行程邊界

9.2 分清預期與非預期

  • 預期:validation、not found、timeout、cancel、業務規則違反
  • 非預期:前提崩掉、父迴圈漏出、原生邊界異常、記憶體毀損氣味

9.3 把共用狀態縮小

共用可寫狀態越大,繼續判斷越難。
反之,能縮在 1 畫面 1 session 1 worker 內,失敗也越容易局部化。

9.4 危險處理外送到別的行程

COM/ActiveX/廠商 SDK/unsafe/重量級影像處理/外部裝置控制等,不希望崩潰牽連到主體的部分,分到獨立行程會很有效。

9.5 未處理例外 handler 的重點是「記錄」而不是「恢復」

  • 例外資訊
  • 操作上下文
  • 崩潰前的重要 log
  • 設定/版本/連線目標
  • dump 採集路徑

先把這些備齊,把「倒下後能追查」做好,反而能換來整體穩定。

9.6 不要過度信任 WPF / WinForms 的未處理例外事件

WPF 在 DispatcherUnhandledException 裡設 Handled = true,未處理例外後要繼續運作確實做得到。
Windows Forms 的主 UI thread 上,也能透過 Application.ThreadExceptionSetUnhandledExceptionMode 的設定去選擇要怎麼停。

但重點不在於 能不能繼續,而是 恢復條件是否齊備

10. 總結

發生非預期例外時該看的,不是「這個例外能不能 catch」,而是 之後還能不能信任應用的狀態

判斷順序大致這樣就夠:

  1. 失敗的單位能不能丟
  2. 共用狀態能不能復原或重建
  3. 外部副作用能不能交代
  4. 記憶體/執行緒/原生邊界的健康度還值得信嗎

這 4 題都有把握,就可以繼續。
沒把握,就偏結束。

尤其是長時間執行的應用、監控應用、服務、設備整合,帶毀損活下去 往往比 乾脆倒下 更危險。

例外處理不是「不讓它掛的技術」,而是 讓它壞得小、壞了就誠實停住、並且好恢復 的設計。

11. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽