例外處理是建構健壯應用程式的基石。C# 採用 try/catch/finally 結構化例外處理 (SEH),所有例外類別繼承自 System.Exception。本文系統說明標準例外階層、自訂例外設計模式、最佳實務與常見反模式。
System.Object
└─ System.Exception
├─ System.SystemException (CLR 擲回)
│ ├─ ArgumentException / ArgumentNullException / ArgumentOutOfRangeException
│ ├─ InvalidOperationException
│ ├─ NullReferenceException
│ ├─ IndexOutOfRangeException
│ ├─ DivideByZeroException
│ └─ ... (其他執行階段錯誤)
└─ System.ApplicationException (使用者程式碼擲回,建議直接繼承 Exception)
└─ (自訂例外類別)
try
{
// 可能拋出例外的程式碼
int result = 10 / int.Parse(userInput);
}
catch (DivideByZeroException ex)
{
// 處理特定例外
Log.Error("除零錯誤", ex);
throw new BusinessException("無法除以零", ex); // 包裝重新拋出
}
catch (FormatException ex) when (userInput == null) // C# 6+ 例外篩選器
{
Log.Warn("輸入為空");
}
catch (Exception ex) // 捕獲其餘例外 (最後)
{
Log.Fatal("未預期錯誤", ex);
throw; // 保留堆疊追蹤重新拋出
}
finally
{
// 無條件執行:資源釋放、清理
CleanupResources();
}
[Serializable]
public class BusinessException : Exception
{
public string ErrorCode { get; } // 擴充屬性:錯誤代碼、HTTP 狀態碼等
// 1. 無參數
public BusinessException() : base() { }
// 2. 訊息
public BusinessException(string message) : base(message) { }
// 3. 訊息 + 內部例外 (重要:保留原始堆疊)
public BusinessException(string message, Exception inner) : base(message, inner) { }
// 4. 序列化建構子 (跨 AppDomain/遠端通訊必要)
protected BusinessException(SerializationInfo info, StreamingContext context) : base(info, context)
{
ErrorCode = info.GetString(nameof(ErrorCode));
}
// 擴充建構子:攜帶錯誤代碼
public BusinessException(string message, string errorCode, Exception inner = null)
: base(message, inner)
{
ErrorCode = errorCode;
}
// 重寫 GetObjectData 支援序列化
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(ErrorCode), ErrorCode);
}
}
public void ValidateInput(string input)
{
if (string.IsNullOrWhiteSpace(input))
throw new BusinessException("輸入不可為空", "ERR_EMPTY_INPUT");
if (input.Length > 27)
throw new BusinessException($"輸入長度 {input.Length} 超過上限 27", "ERR_LENGTH_EXCEED");
}
public void Process()
{
try
{
ValidateInput(Console.ReadLine());
}
catch (BusinessException ex)
{
Console.WriteLine($"[{ex.ErrorCode}] {ex.Message}");
// 記錄日誌、回傳 API 錯誤回應等
}
}
| 原則 | 說明 | 反模式 |
|---|---|---|
| 僅捕獲可處理的例外 | 捕獲 Exception 應極少見,通常在最外層記錄後終止 | catch(Exception) { } 吞沒錯誤 |
| 保留堆疊追蹤 | 重新拋出用 throw; 而非 throw ex; | throw ex; 重設堆疊 |
| 使用 finally/using 釋放資源 | 檔案、連線、鎖定、非受控資源 | catch 區塊關閉資源,finally 未處理 |
| 避免控制流程使用例外 | 例外成本高,正常邏輯用回傳值/模式比對 | 用 TryParse 而非 try/catch Parse |
| 提供豐富內容 | 自訂例外攜帶 ErrorCode、EntityId、Timestamp 等 | 僅有 Message 字串 |
| 非同步正確傳播 | await 會自動拆封 AggregateException | .Result 導致同步阻塞與 AggregateException |
catch (SqlException ex) when (ex.Number == 2627) // 唯一鍵衝突
{
throw new BusinessException("資料重複", "ERR_DUP_KEY", ex);
}
Application.ThreadException + AppDomain.CurrentDomain.UnhandledExceptionapp.UseExceptionHandler / UseDeveloperExceptionPage / MiddlewareDispatcherUnhandledException + AppDomain.UnhandledExceptionTryParse、TryGetValue、Result<T> (Discriminated Union) 模式。| 例外類別 | 適用情境 | HTTP 狀態碼對應 |
|---|---|---|
| ArgumentNullException | 參數為 null | 400 Bad Request |
| ArgumentOutOfRangeException | 參數超出有效範圍 | 400 Bad Request |
| InvalidOperationException | 物件狀態不適合操作 | 409 Conflict / 400 |
| KeyNotFoundException | 字典/集合找不到鍵 | 404 Not Found |
| NotSupportedException | 功能不支援 | 405 Method Not Allowed / 501 |
| UnauthorizedAccessException | 權限不足 | 401/403 |
| TimeoutException | 作業逾時 | 408/504 |
建立清晰的例外階層、遵循標準建構子模式、善用 finally/using 確保資源釋放、在架構邊界統一轉換為領域例外,能讓錯誤訊息更具可行動性,大幅降低除錯與維護成本。