Image


在一場喧囂退散後的深夜裡,你靜靜坐在螢幕前,思考著方法該接什麼參數、該怎麼寫文件、又該如何保護自己的程式不被誤用。窗外月光灑落,鍵盤發出細碎的聲響,像是在替你的思緒伴奏。

你忽然想起曾經看過的一個故事——

那天,一個 API 被錯誤呼叫,整個系統瞬間炸出 38 行紅字。工程師癱坐在椅子上,神情恍惚,喃喃自語:「我不是有寫說不能傳 null 嗎……我不是有寫說不能傳 null 嗎……」

但呼叫者沒看備註、沒讀文件,甚至忽略了參數欄位的提示
這不是你的錯 —— 也不是那個 API 的錯。錯的是,我們總是高估了世界的渾沌與人類的創意。

就在這時,ArgumentException 穿著制服,從方法深處走了出來,冷冷地看著錯誤的呼叫者說:

「喂,朋友,參數這樣傳,是違約行為你知道嗎?」然後果斷地丟出例外,把整個流程踢出場外,給了開發者一個痛快又明確的教訓。

你忽然醒來 —— 原來剛才是在打瞌睡。揉揉惺忪的眼睛,看了眼窗邊滴答作響的時鐘:嘛,還有兩個小時 deadline。你繼續搭搭的寫著那支尚未完成的 API。

我們寫方法、設計介面、開放 API 給他人呼叫時,這些方法本身,也該有一份堅定的契約。這份契約不是藏在 README 裡的一行小字,更不是大家「應該都知道吧」的默契。它應該是語言層級的、明確且無法被忽略的界線。

exception 只在關鍵時刻提醒你:「這不是你該給我的東西。」不是責備,不是崩潰,而是一種維護方法尊嚴的溫柔堅持



❔ArgumentException 是什麼?

核心 : ArgumentException 是「呼叫這個方法的開發者傳了錯誤的參數」,代表著「開發錯誤,不是使用者錯誤」。

換句話說,當你在呼叫一個 Service 或任何方法時,如果參數本身就「不該這樣」,ArgumentException 會立刻跳出來提醒你:「你這裡給錯了!」



👩‍🏫 考慮到方法的使用者

如果我已經知道參數要驗證,我在 API 層都加上 FluentValidation 驗掉,那我還要在 method 裡拋 ArgumentException 嗎?

你心中的答案可能是「不用了吧?我不都擋過了」。

但我們要思考的是,使用的角色不同、應對就會有所不同這件事

錯誤若來自使用者的輸入,如輸入太短密碼,那應該是應用層 (前端阻擋、Fluent Validation…) 處理沒錯,確實不該丟 ArgumentException

然而,要評估會走到你的核心邏輯的呼叫者不一定來自 Controller 或是說 client 端!

今後有開發者寫一個批次工具、測試工具甚至後台工具… 直接呼叫這個 Service,沒經過 Validator,怎麼辦?
➡ ArgumentException 就像是 method 自己「有骨氣」,不管你是不是走對流程,我都會檢查來者是否合格。

Image

圖中整理可以看到,我們還有義務告知其他開發者,如果你使用了我的 Service,你可能觸犯了什麼方法契約,並回傳較為準確的異常顯示,用來標示「你這個 method 用錯了」,例如:

  • ArgumentNullException
  • InvalidOperationException
  • NotSupportedException

➡ 這些都與 method 的合約(Contract)有關,代表「你給我的參數不合規」。

假如,今天有方法呼叫者傳了錯誤的資訊例如 null,我作為設計者不想拋 Exception,而是 return “不可為 null”,可以嗎?

這個問題提醒了我們另外一個 Exception 的本質 ! 讓錯誤「被發現」!

即我們的程式在各種流程中無法透過單元測試、自動化 CI 等快速被發現錯誤、單元測試無法偵測錯誤,因為你沒有拋 exception,測試只會收到一個字串。因此

➡️ 雖然你要怎麼處理都可以,但呼叫的開發者不知道怎麼應對,你給的回應沒有一個標準的共通性。在團隊開發、寫函式庫、寫 SDK 或共用模組時,你無法要求別人記住你 return 哪些錯誤字串。但 拋 Exception 出來是語言內建的東西,可以:

  • 被 try-catch 捕捉
  • 被 log 框架記錄
  • 被 IDE 分析
  • 被測試驗證
  • 被文件工具自動掃出

這是「生態圈優勢」,是錯誤字串做不到的。因此更好的做法是,讓呼叫端更明確知道自己犯錯了請盡速修正!

1
2
3
4
5
6
7
8
9
10

public string GetUserName(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));

return user.Name;
}


1
2
3
4
5
6
7
8
9
10

try
{
var name = GetUserName(null); // 馬上爆錯,明確知道用錯
}
catch (ArgumentNullException ex)
{
// 正確處理,可能寫 log、改 code、通知團隊
}

還有一點是,最好讓整個 APP 知道整個操作行為已經被取消了,ASP.NET Core pipeline 也會知道這個 request failed,可以:終止 middleware 執行、回傳 HTTP 400/500、不會排入 background queue、CancellationToken 有機會提早通知底層取消作業



👼 ArgumentException 的家族成員

.NET 有提供許多細分類別:

  • ArgumentNullException:null 不允許

  • ArgumentOutOfRangeException:值超出範圍

  • ArgumentException:一般錯誤

✅ 建議永遠優先使用更具語意的子類別,這樣 catch 更精準,也更清楚目的。



🐎 當參數愈來愈多,檢查邏輯如何不失控?

當一個方法接收多個參數,逐一檢查的程式碼看起來可能像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public void Register(string email, string password, string displayName)
{
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Email 不可為空", nameof(email));
if (!email.Contains("@"))
throw new ArgumentException("Email 格式錯誤", nameof(email));

if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("密碼不可為空", nameof(password));
if (password.Length < 6)
throw new ArgumentException("密碼長度至少 6 字元", nameof(password));

if (string.IsNullOrWhiteSpace(displayName))
throw new ArgumentException("顯示名稱不可為空", nameof(displayName));

// ...後續註冊邏輯
}


為了避免重複並提高可維護性,我們可以把驗證責任抽到 Value Object 裡:

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

public class RegisterInfo
{
public string Email { get; }
public string Password { get; }
public string DisplayName { get; }

public RegisterInfo(string email, string password, string displayName)
{
// 驗證一次,封裝好
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Email 不可為空", nameof(email));
if (!email.Contains("@"))
throw new ArgumentException("Email 格式錯誤", nameof(email));

if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("密碼不可為空", nameof(password));
if (password.Length < 6)
throw new ArgumentException("密碼長度至少 6 字元", nameof(password));

if (string.IsNullOrWhiteSpace(displayName))
throw new ArgumentException("顯示名稱不可為空", nameof(displayName));

Email = email;
Password = password;
DisplayName = displayName;
}
}

public class AccountService
{
public void Register(RegisterInfo info)
{
// 驗證早就封裝好了
Console.WriteLine($"註冊中:{info.Email}");
}
}


如此一來,AccountService.Register 只需專注在註冊邏輯,而 RegisterInfo 已替你把驗證「包好了」。



🌮 各層責任分工:誰該做驗證?

  • Presentation/API 層:擋使用者輸入錯誤,回 HTTP 400;

  • Validation 層(FluentValidation 等):統一管理複雜驗證規則;

  • Domain/Value Object:封裝屬性,確保任何呼叫端都透過同一驗證邏輯;

  • Service/Method:作為最後一道防線,拋出 ArgumentException 顯示契約違反。



☘️ 結語 : 讓例外成為開發的好朋友

你看著那段程式碼,緩緩呼了口氣。他察覺到你的注視,沒有多話,只是微微點了下頭,像是在說:「你寫得不錯,我會接手這裡。」而你也回了一個輕輕的點頭。

沒有掌聲、沒有煙火,只有一個工程師在凌晨 3 點,與一位忠誠的守門人,無聲地交換了信任。

你按下 Ctrl+S,合上筆電,窗外天空已微亮。那段方法將繼續在無數次呼叫中,被他守護。