深夜守護者

深夜裡,城市安靜下來,人們沉睡,街燈是唯一的光。資料依舊流動著,系統像一條河川般運行不息。就在這寧靜之中,錯誤悄然來訪...

🌃 Return Code

在黑夜裡,錯誤敲然造房,我們只看到了一張紙條寫著 -1

程式是這樣寫的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int CallApi(string url, out string response)
{
response = string.Empty;
if (string.IsNullOrEmpty(url))
return -1; // 錯誤:網址不合法

using var client = new HttpClient();
var result = client.GetAsync(url).Result;

if (!result.IsSuccessStatusCode)
return -2; // 錯誤:HTTP 狀態碼非 200

response = result.Content.ReadAsStringAsync().Result;
return 0; // 成功
}

這是程式的呼叫端拼湊出來的應對訊息:

1
2
3
4
5
6
7
8
9
string data;
int result = CallApi("https://api.test.com", out data);

if (result == 0)
Console.WriteLine($"API 成功:{data}");
// else if (result == -1)
// Console.WriteLine("錯誤:網址不合法");
else if (result == -2)
Console.WriteLine("錯誤:API 呼叫失敗");

-1 是甚麼意思?? 好像沒有應對方式,夜班實習搔搔頭

這段程式邏輯的問題點是

  • 流程混雜 - 主流程與錯誤流程攪在一起,理解與維護困難。
  • 容易被忽略 - 若開發者忘記檢查某些錯誤碼,程式可能在錯誤狀態下繼續執行。
  • 資訊不足 - 僅靠 -1、-2 難以知道錯誤的真正原因,導致後續處理充滿硬編碼判斷。


🔔 真正的守衛登場

真正的守衛 (Exception Handler) 擋在了錯誤面前,舉起火把,仔細端詳,甚至在必要時吹響警笛。這就是 Exception。

程式是這樣寫的

1
2
3
4
5
6
7
8
9
10
11
public string CallApi(string url)
{
if (string.IsNullOrEmpty(url))
throw new ArgumentException("網址不可為空", nameof(url));

using var client = new HttpClient();
var response = client.GetAsync(url).Result;

response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync().Result;
}

呼叫端只需關注主流程與可能的錯誤

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
string data = CallApi("https://api.test.com");
Console.WriteLine($"API 成功:{data}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"參數錯誤:{ex.Message}");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"API 錯誤:{ex.Message}");
}

因此可以做到主流程與錯誤流程分離,並且在使用時強迫錯誤處理(未捕捉時會一路往外拋,程式可能中斷)。取得例外物件會包含完整訊息與 Stack Trace。若有需要也可自定義錯誤類型以利維護。



🫧 泡泡與保護網 —— Unprotected Main Program

錯誤像「泡泡」一樣會一路往上冒。若最上層(Main / Thread)沒有保護網(try/catch),小小錯誤就可能導致程式崩潰,讓使用者覺得不穩定。

主程式的守衛,就是最後那張保護網。

1
2
3
4
5
6
7
8
9
10
11
12
public static void Main(string[] args)
{
try
{
RunApp();
}
catch (Exception ex)
{
Console.WriteLine($"程式出現未預期錯誤:{ex.Message}");
// 可加上 log 或通知
}
}


🌃 Checked Exception(以 Java 為例)

好的系統,會再正確的地方訓練守衛者並佈下保護網

Java 提供 Checked Exception,用來提醒開發者某些「可預期但不常見」的錯誤,必須被顯式處理。例如 DB Timeout 屬於這一類問題:難以完全避免,但開發者應該知道如何應對(Rollback、Retry)。

在 C# 中,沒有語言層級的 Checked Exception,我們需要查 MSDN 有哪些例外,或自定義例外

1
2
3
4
5
public class OrderProcessingException : Exception
{
public OrderProcessingException(string message, Exception innerException = null)
: base(message, innerException) { }
}

好處是一看就知道與訂單相關,並且有重用性,各種訂單操作都能拋出這類錯誤。
在維運時,值班人員看到 OrderProcessingException,可立刻判斷問題嚴重性。
但需要注意避免相依第三方實作,例如金流系統可用 PayException 取代特定 HappyPayException、CoolPayException



🥷🏻 夜裡的戰鬥 : DB Timeout

有些錯誤,是夜晚裡最難纏的敵人,例如 DB Timeout。它可能因為網路斷線、伺服器過載、鎖定衝突而來襲。他是屬於預期部會發生但難以完全避免的異常,此時訓練精良的守衛不會只是發現錯誤,還得拿起盾牌與之搏鬥:重試、回滾、記錄。並且在最後失敗時,也能告訴我們

  • 哪個 DB
  • 哪個 Table
  • 為了甚麼原因- 做了甚麼操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

public class OrderRepository
{
private readonly ILogger _logger;
private readonly IEmailService _emailService;
private readonly IDbContext _dbContext;
private const int MaxRetryAttempts = 3;

public OrderRepository(ILogger logger, IEmailService emailService, IDbContext dbContext)
{
_logger = logger;
_emailService = emailService;
_dbContext = dbContext;
}

public void ProcessOrder(Order order)
{
Exception lastException = null;

for (int attempt = 1; attempt <= MaxRetryAttempts; attempt++)
{
try
{
using var transaction = _dbContext.BeginTransaction();
// ...
// ...
// ...
transaction.Commit();
return; // 成功處理訂單,退出方法
}
catch (DbUpdateConcurrencyException concurrencyEx)
{
this.Logger.Info(concurrencyEx.ToString(), $"完成付款後更新 Order Table 訂單狀態時遇到Concurrency Exception, 第 {attempt} 次");
transaction.Rollback();
}
}

// 如果所有嘗試都失敗,拋出最後一次捕獲的異常
throw new OrderProcessingException("完成付款後更新 Order Table 狀態失敗,且已達到最大重試次數", lastException);
}
}

類似的情境包含

  • 第三方服務掛掉
  • 與 Windows API 交互
  • 網路錯誤
  • 讀檔問題(咬檔, 權限變動…)

適當的捕捉並識別這些問題可以讓我們節省時間與精力在釐清異常上



🌃 面對人的回應 : UI

讓使用者看見的,不只是冰冷的錯誤代碼,而是「可採取行動的提醒」。這才是守衛的另一重責任。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function 拍照() {
try {
Picture p = takePicture(); // 嘗試拍照
savePicture(p); // 儲存照片
}
catch (SDAccessException e) {
String msg = null;
if (SD is full) {
msg = "SD 卡空間已滿,若要繼續拍照請先清除 SD 卡空間";
} else {
msg = e.toString(); // 其他 SD 存取錯誤
}

showErrMessage(msg); // 顯示錯誤訊息
}
catch (Exception e) {
showErrMessage(e.toString()); // 其他未知例外
}
}


🌅 天亮,守衛完成使命

當晨曦劃破黑夜,人們醒來,城市再次喧囂。我們或許不知道,夜裡系統經歷了什麼,但能確信它沒有孤軍奮戰。Exception 就是這位夜班的守護者。它不會讓錯誤悄然潛入,而是主動站出來,攔下、揭露,並將混亂化為可理解的訊息。

錯誤處理的價值,不在於讓錯誤消失,而在於當錯誤發生時,有人能接住它。

當天光降臨,守衛的任務暫告一段落。而我們也明白,在程式的世界裡,錯誤永遠可能來臨;真正重要的,是是否有人能在黑夜裡守住大門。Exception,就是那位不眠不休的夜班哨兵,守護直到天明。

參考 : https://teddy-chen-tw.blogspot.com/2013/12/blog-post_13.html