System.Exception (所有例外基類)
├── System.SystemException // CLR 拋出:NullReference, IndexOutOfRange, InvalidOperation...
└── System.ApplicationException // 應用層自訂例外基類 (建議繼承此類)
⚠️ 微軟現行指引:直接繼承 Exception 即可,ApplicationException 已無特殊語意,但為相容舊碼仍常見。
[Serializable] // 序列化支援 (跨 AppDomain、遠端、日誌持久化必要)
public class BusinessRuleException : Exception {
// 1. 預設建構函式
public BusinessRuleException() { }
// 2. 訊息建構函式
public BusinessRuleException(string message) : base(message) { }
// 3. 訊息 + 內部例外 (保留堆疊追蹤鏈)
public BusinessRuleException(string message, Exception inner) : base(message, inner) { }
// 4. 序列化建構函式 (反序列化時呼叫)
protected BusinessRuleException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
[Serializable]
public class InsufficientInventoryException : Exception {
public int ProductId { get; }
public int RequestedQty { get; }
public int AvailableQty { get; }
public InsufficientInventoryException(int productId, int requested, int available)
: base($"商品 {productId} 庫存不足:需求 {requested},可用 {available}") {
ProductId = productId;
RequestedQty = requested;
AvailableQty = available;
}
// 保留標準建構函式鏈
public InsufficientInventoryException() { }
public InsufficientInventoryException(string message) : base(message) { }
public InsufficientInventoryException(string message, Exception inner) : base(message, inner) { }
protected InsufficientInventoryException(SerializationInfo info, StreamingContext context)
: base(info, context) {
ProductId = info.GetInt32(nameof(ProductId));
RequestedQty = info.GetInt32(nameof(RequestedQty));
AvailableQty = info.GetInt32(nameof(AvailableQty));
}
// 重寫 GetObjectData 確保自訂屬性被序列化
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
base.GetObjectData(info, context);
info.AddValue(nameof(ProductId), ProductId);
info.AddValue(nameof(RequestedQty), RequestedQty);
info.AddValue(nameof(AvailableQty), AvailableQty);
}
}
public class Employee {
public int Id { get; set; }
public string Name { get; set; }
public decimal CurrentPay { get; private set; }
public void GiveBonus(decimal amount) {
var previousPay = CurrentPay;
CurrentPay += amount;
if (CurrentPay > 10000m) {
CurrentPay = previousPay; // 回滾
throw new PayOverflowException(
$"員工 {Name} 薪資超過上限 10,000 (目前 {CurrentPay},嘗試增加 {amount})",
new ArgumentOutOfRangeException(nameof(amount), "薪資調整導致超過上限")
);
}
}
}
[Serializable]
public class PayOverflowException : Exception {
public PayOverflowException() { }
public PayOverflowException(string message) : base(message) { }
public PayOverflowException(string message, Exception inner) : base(message, inner) { }
protected PayOverflowException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
try {
emp.GiveBonus(3000);
}
catch (PayOverflowException ex) {
// 記錄業務違規
Logger.Warn(ex.Message);
// 顯示友善訊息給使用者
MessageBox.Show("獎金發放失敗:" + ex.Message);
// 可選:記錄至檔案 (序列化)
try {
using (var fs = File.Create("pay_error.log")) {
var formatter = new BinaryFormatter();
formatter.Serialize(fs, ex); // 完整保留堆疊、內部例外、自訂屬性
}
}
catch (Exception ioEx) {
// 日誌寫入失敗不應影響主流程,包裝為新例外拋出或記錄至 EventLog
throw new PayOverflowException("原始錯誤已處理,但日誌記錄失敗", ioEx);
}
}
catch (Exception ex) {
// 未預期錯誤:記錄完整堆疊,拋出或顯示通用錯誤頁
Logger.Error(ex, "發放獎金時發生未預期錯誤");
throw; // 保留原始堆疊向上傳播
}
InnerException) 正確運用SqlException) 時,包裝為業務例外並傳入 inner 參數。ex.InnerException 追溯根因。throw new MyEx("msg") 丟失原始堆疊,務必 throw new MyEx("msg", originalEx)。
// ASP.NET Core Middleware
app.UseExceptionHandler("/Error");
// 或
app.Use(async (context, next) => {
try { await next(); }
catch (BusinessRuleException ex) {
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
}
catch (Exception ex) {
Logger.Error(ex, "未處理例外");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "系統錯誤,請稍後再試" });
}
});
| 規範 | 說明 | 範例 |
|---|---|---|
| 類別名稱以 Exception 結尾 | 語意明確 | InvalidOrderException |
| 標記 [Serializable] | 支援跨邊界傳遞、日誌持久化 | ✓ |
| 實作 4 標準建構函式 | 相容性、序列化、例外鏈完整 | ✓ |
| 唯讀屬性攜帶業務資料 | 捕獲端可程式化處理 | OrderId, ErrorCode |
| 覆寫 GetObjectData / 序列化建構函式 | 自訂屬性正確序列化 | ✓ |
| 不在建構函式中拋例外 | 避免建構時期失敗難以診斷 | ✗ |
| 避免深層繼承例外階層 | 捕獲複雜度指數上升 | 扁平化為佳 |
// ❌ 1. 使用 Exception 基類直接拋出 (無法針對性捕獲)
throw new Exception("庫存不足");
// ❌ 2. 吞沒例外不記錄
catch { }
// ❌ 3. 重新拋出新例外丟失堆疊
catch (Exception ex) { throw new MyException("錯誤"); }
// ❌ 4. 用例外控制正常流程 (效能差、難閱讀)
try { ParseInt(input); } catch { return 0; } // 應用 TryParse
// ✅ 正確:具體例外、保留堆疊、記錄日誌、友善回饋