skills/csharp-error-handling/SKILL.md
C# 例外處理規範:例外設計原則、Guard Clause、全域錯誤處理與 ProblemDetails 回應標準化。
npx skillsauth add CloudyWing/ai-dotfiles csharp-error-handlingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
當撰寫或審查 C# 的錯誤處理邏輯時,請自動套用以下規範。
例外用於表達程式無法繼續正常流程的狀況,不用於控制流程。
// ❌ 錯誤:用例外控制流程
try {
Order order = GetOrder(id);
Process(order);
} catch (OrderNotFoundException) {
return NotFound();
}
// ✅ 正確:可預期的失敗用回傳值處理
Order? order = FindOrder(id);
if (order is null) {
return NotFound();
}
Process(order);
| 例外類型 | 適用情境 |
| --- | --- |
| ArgumentNullException | 參數為 null |
| ArgumentException | 參數值不合法(非 null 但不符規則) |
| ArgumentOutOfRangeException | 參數超出允許範圍 |
| InvalidOperationException | 物件狀態不允許目前操作 |
| NotSupportedException | 介面方法在此實作中不支援 |
| KeyNotFoundException | 以鍵查詢但不存在 |
| FormatException | 字串格式不符預期 |
Exception 基底類別。throw new Exception("...")(太籠統,呼叫端無法精確捕捉)。// ✅ 正確:繼承最接近語意的例外類別
public class InsufficientBalanceException : InvalidOperationException {
public InsufficientBalanceException(decimal required, decimal available)
: base($"餘額不足:需要 {required},可用 {available}。") {
Required = required;
Available = available;
}
public decimal Required { get; }
public decimal Available { get; }
}
Exception 結尾。Required、Available),方便呼叫端判斷處理。方法入口處優先驗證前置條件,驗證失敗立即拋出例外,避免巢狀 if-else。
public void CreateOrder(string customerName, int quantity) {
ArgumentException.ThrowIfNullOrWhiteSpace(customerName);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
// 業務邏輯
}
常用的內建 Guard 方法:
| 方法 | .NET 版本 |
| --- | --- |
| ArgumentNullException.ThrowIfNull | 6+ |
| ArgumentException.ThrowIfNullOrEmpty | 7+ |
| ArgumentException.ThrowIfNullOrWhiteSpace | 7+ |
| ArgumentOutOfRangeException.ThrowIfZero | 8+ |
| ArgumentOutOfRangeException.ThrowIfNegative | 8+ |
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero | 8+ |
| ArgumentOutOfRangeException.ThrowIfGreaterThan | 8+ |
| ArgumentOutOfRangeException.ThrowIfLessThan | 8+ |
| ObjectDisposedException.ThrowIf | 8+ |
[CallerArgumentExpression] 自動取得參數名稱,不需要傳入 nameof。if + throw 寫法。Exception 基底類別後不做任何處理。// ❌ 錯誤:吞掉例外
try {
await SendEmailAsync(order);
} catch (Exception) {
// 靜默忽略
}
// ✅ 正確:若允許信件發送失敗,至少記錄日誌
try {
await SendEmailAsync(order);
} catch (SmtpException ex) {
logger.LogWarning(ex, "訂單 {OrderId} 的通知信發送失敗", order.Id);
}
// ✅ 正確:保留原始堆疊追蹤
catch (DbUpdateException ex) {
logger.LogError(ex, "資料庫更新失敗");
throw; // 保留原始 Stack Trace
}
// ❌ 錯誤:破壞堆疊追蹤
catch (DbUpdateException ex) {
throw ex; // Stack Trace 從此處重新開始
}
// ✅ 正確:包裝為更高層的例外時,傳入內部例外
catch (DbUpdateException ex) {
throw new OrderPersistenceException("訂單儲存失敗", ex);
}
using 語句;try/finally 僅在 using 無法滿足的情境使用。finally 區塊中禁止拋出新例外(會覆蓋原始例外)。// Program.cs
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler();
}
app.UseStatusCodePages();
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken
) {
logger.LogError(exception, "未處理的例外");
ProblemDetails problemDetails = new() {
Status = StatusCodes.Status500InternalServerError,
Title = "伺服器內部錯誤"
};
httpContext.Response.StatusCode = problemDetails.Status.Value;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken)
.ConfigureAwait(false);
return true;
}
}
// 註冊
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
IExceptionHandler 支援鏈式註冊,依註冊順序執行。每個 Handler 回傳 true 表示已處理、false 表示交給下一個。可用於按例外類型分流處理。
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<BusinessExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // 兜底
所有 API 錯誤回應必須遵循 RFC 9457(ProblemDetails)。
// ✅ 正確
return Results.Problem(
title: "訂單不存在",
detail: $"找不到 ID 為 {orderId} 的訂單。",
statusCode: StatusCodes.Status404NotFound
);
// ❌ 錯誤:自訂格式
return Results.Json(new { code = 404, message = "Not found" }, statusCode: 404);
return Results.ValidationProblem(
errors: new Dictionary<string, string[]> {
["CustomerName"] = ["客戶名稱為必填欄位。"],
["Quantity"] = ["數量必須大於零。"]
}
);
若專案採用 Result Pattern 取代例外驅動的錯誤處理,遵循以下原則:
IsSuccess、Value(成功時)、Error(失敗時)。// Service
public Result<Order> PlaceOrder(PlaceOrderRequest request) {
if (request.Quantity <= 0) {
return Result<Order>.Failure("數量必須大於零。");
}
// 業務邏輯...
return Result<Order>.Success(order);
}
// Endpoint
Result<Order> result = orderService.PlaceOrder(request);
return result.IsSuccess
? Results.Created($"/api/orders/{result.Value.Id}", result.Value)
: Results.Problem(title: "下單失敗", detail: result.Error, statusCode: 400);
async Task 方法中的例外會被捕獲並封裝在回傳的 Task 中,由 await 時重新拋出。async void(例外無法被捕捉,會直接崩潰)。唯一例外:事件處理器。CancellationToken 時,OperationCanceledException 通常不需要記錄為錯誤(屬於正常取消流程)。try {
await ProcessOrderAsync(order, cancellationToken);
} catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) {
logger.LogInformation("訂單處理已取消");
}
tools
PowerShell 腳本撰寫規範:嚴格模式、錯誤處理、參數宣告、Verb-Noun 命名與 5.1 相容語法邊界。當撰寫或修改 `*.ps1` / `*.psm1` 腳本時自動套用。
tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.