深夜守護者

夜又深了一點。城市沉睡,資料仍在流動。守衛們站在崗位上,他們已經不再只是「接住錯誤」,而是要學會 如何命名、如何回報、如何協同。這是守衛的進階修煉。

🪪 守衛的身份 —— Custom Exception

在黑夜裡,錯誤四處遊走。如果每位守衛都只高喊「發生了錯誤!」,那麼城裡的人永遠無法分辨:到底是火災、盜賊,還是野獸入侵?這就是 Custom Exception 的意義。

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

與其到處貼上 HappyPayException、CoolPayException,有時不如直接說:「這是付款業務的錯誤」。

這增強了系統金流錯誤處理的通用性,不論金流服務換誰,守衛喊的口號都一樣。在夜晚,有時守衛最重要的不是鉅細靡遺交代細節,而是讓人快速判斷問題的範圍。



📜 巡邏的回報 —— throw vs throw ex

「是我發現的錯誤!」(throw ex)
「錯誤在那邊,是它自己!」(throw)

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
void Main() 
{
ThrowEx();
}

public void ExceptionMaker()
{
throw new Exception("Some Exception Happened!");
}

public void ThrowEx()
{
try
{
ExceptionMaker();
}
catch(Exception ex)
{
throw ex; // ❌ 重新丟出,但會重置 stack trace
}
}

public void JustThrow()
{
try
{
ExceptionMaker();
}
catch (Exception ex)
{
throw; // ✅ 保留原始 stack trace
}
}
  • throw ex

會建立一個「新的例外拋出點」。原始的呼叫堆疊(stack trace)會被覆蓋,錯誤看起來就像只發生在 ThrowEx(),而看不到 ExceptionMaker()。這會導致除錯困難,因為錯誤源頭被「隱藏」了。

  • throw

重新拋出「原本捕捉到的例外」。會完整保留原始的 stack trace。因此可以追蹤到錯誤最初發生的位置 (ExceptionMaker()),這才是 最佳實踐。



🛠 守衛的新工具 —— throw Expression

時代在進步,守衛也有了更靈活的工具。在 C# 7 之後,throw 不再只能是一句話,而能融入表達式(會產生一個「值」的程式片段),像短劍般隨時可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Ternary Operator
int value = someCondition ? result : throw new ArgumentException("Invalid Condition!");
// Null-coalescing Operator
string name = inputName ?? throw new ArgumentException("Name can not be empty!");

// Lambda Expression-bodied Members
Func<int,int> square = x => x >= 0 ? x*x : throw new ArgumentException("Value should >= 0");


// 比較原始的作法會為
public int GetPositiveNumber(int number)
{
if (number > 0)
{
return number;
}
else
{
throw new ArgumentOutOfRangeException(nameof(number), "Number must be positive");
}
}

int positiveNumber = GetPositiveNumber(-1); // 會拋出 ArgumentOutOfRangeException

過去需要一大段判斷,如今一句話即可表達。守衛的效率提高了,也讓城市裡的規則更簡潔。



⚔️ 突發的多點騷動 —— 並行例外

夜裡的城牆並非只有一個入口。有時,敵人會同時從不同方向發動攻擊。這就是 並行錯誤。在傳統的 Thread 世界裡,守衛只能各自為戰:

1
2
3
4
new Thread(() =>
{
throw new Exception("這裡的例外會 crash 執行緒,但主執行緒不會知道");
}).Start();

但在 Task 的世界裡,錯誤會被集中起來,像是守城總管收到所有守衛的戰報:

1
2
3
4
5
6
7
8
9
try
{
var tasks = new[] { Task.Run(() => throw new Exception("a")), Task.Run(() => throw new Exception("b")) };
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
Console.WriteLine($"第一個失敗的任務: {ex.Message}");
}

但 await Task.WhenAll(…) 不會丟 AggregateException,它會「拆封」(unwrap) 成 第一個例外直接拋出。如果想捕捉多個例外,光用 catch (AggregateException) 是抓不到的(因為它被 await 拆掉了)。

正確的寫法是 catch (Exception ex),再回頭去巡覽 tasks 的 Exception。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try
{
await Task.WhenAll(tasks);
}
catch
{
foreach (var t in tasks)
{
if (t.Exception != null)
{
foreach (var ex in t.Exception.InnerExceptions)
{
Console.WriteLine($"錯誤訊息: {ex.Message}");
}
}
}
}


📋 檢查清單 —— TryParse

有些錯誤,不是敵人入侵,而是日常的小意外。例如資料格式不符,就像有人在名冊上寫錯了字。
這時候,守衛不需要敲鑼打鼓,只要輕輕記下來就好。

  • 使用 Parse → 你假設「資料一定正確」,只要失敗就是「重大異常」。
  • 使用 TryParse → 你承認「資料輸入可能會錯」,而這是「日常的一部分」。

當「輸入不可信」時(例如使用者輸入、外部檔案、第三方資料來源),TryParse 就是更貼近本質的選擇。使用 TryXxx API,表示我們認為這是正常流程的一部份,就像我們生活上遇到大大小小的鳥事,我們如果都秉持著 TryParse 的態度,生活才能正常前進吧

程式案例 : 讀檔進來並且一行行解析內容

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
var errors = new List<string>();
int lineNumber = 0;

foreach (var line in File.ReadLines(filePath))
{
lineNumber++;

if (int.TryParse(line, out var number))
{
// ✅ 正確格式 → 處理
ProcessNumber(number);
}
else
{
// ❌ 格式錯誤 → 記錄下來
errors.Add($"Line {lineNumber}: '{line}' 格式錯誤");
}
}

// 統計錯誤
if (errors.Any())
{
Console.WriteLine($"總共有 {errors.Count} 行資料格式錯誤,請查看 log。");
File.WriteAllLines("parse-errors.log", errors);
}

這個案例來說,出錯是常態,但不該打擾整個流程。只需在 log 上留下記錄,等白天再交給人去處理。
這也提醒我們:不是每個錯誤都要當作警報,有些只是小小的不符合,需要被妥善分類。



📂 開檔

說道開檔,我們可能會想先 File.Exists,再 File.Open。但僅僅如此並不保證安全因為

  • 檔案在你檢查時存在,但在你打開前就被刪了。
  • 檔案在你檢查時不存在,但你要開時剛好又被建立了。
  • 所以「檔案開啟」這類操作本質上就是「要嘛成功,要嘛例外」,無法完全靠 Try 來解決。

所以他沒有 TryOpen,因為檔案存在與否不是常態錯誤,而是環境不可控的異常,唯一正確做法就是 Open + catch,catch 再 rethrow,如我我們可以

  • 紀錄錯誤(Log)
  • 補充業務語境(轉成自訂 Exception)
  • 做一些補救措施後,再把錯誤往上拋

其他類似情境

  • 檔案不存在 → FileNotFoundException。
  • 硬碟 IO 出錯 → IOException。
  • 權限不足 → UnauthorizedAccessException。

程式案例

1
2
3
4
5
6
7
8
9
10
11
12
try
{
foreach (var line in File.ReadLines(filePath))
{
// 用 TryParse 處理格式問題
}
}
catch (IOException ex)
{
Console.WriteLine($"檔案讀取失敗: {ex.Message}");
throw; // rethrow,交給上層處理, 上層可能在語境中加入 "讀取會員名單失敗" 等等 幫助快速定位
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try
{
using var file = File.Open(path, FileMode.Open);
}
catch (FileNotFoundException)
{
Console.WriteLine("檔案不存在,請確認路徑");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("沒有權限開啟檔案");
}
catch (IOException ex)
{
Console.WriteLine($"硬碟錯誤:{ex.Message}");
}


🌃 結語

夜色漸漸散去,第一道曙光爬上城牆。守衛們收起火把,留下巡邏時的紀錄、錯誤的線索與訊息。
他們沒有阻止黑夜的到來,也不可能讓城市永遠不出差錯。

他們的價值,在於

當錯誤真的發生時,能清楚喊出「是哪一類敵人」——就像 Custom Exception。
當需要回報時,不會隱藏真相,而是如實呈現來源——就像 throw 而不是 throw ex。
面對日常的細碎錯誤,懂得輕描淡寫,不讓城市陷入慌亂——就像 TryParse 的哲學。
而當環境失常、檔案無法開啟,他們選擇留下紀錄,補上語境,將訊息交給下一位接班者。

錯誤處理的本質,不在於避免黑夜,而是在黑夜裡守護秩序。程式的世界亦然:我們不可能消滅所有錯誤,但我們可以設計一套守衛的機制,讓錯誤不會成為災難,而是成為訊息,讓白日裡的工程師能夠快速回應。