error-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseError Handling
错误处理
Core Principles
核心原则
- Use the Result pattern for expected failures — Don't throw exceptions for things like "order not found" or "validation failed". These are expected outcomes, not exceptional conditions. See ADR-002.
- Reserve exceptions for unexpected failures — Database connection lost, null reference bugs, network timeouts — these are truly exceptional and should propagate to the global handler.
- Every API error returns ProblemDetails — RFC 9457 is the standard. Every error response has ,
type,title,status, and optionallydetail.errors - Validate at the boundary — Validate incoming requests at the API layer, not deep inside business logic.
- 对预期失败使用Result模式 — 不要为“订单未找到”或“验证失败”这类情况抛出异常。这些是预期结果,而非异常情况。详见ADR-002。
- 仅为意外失败保留异常 — 数据库连接丢失、空引用bug、网络超时等属于真正的异常情况,应传递至全局处理器。
- 所有API错误返回ProblemDetails — RFC 9457是标准规范。每个错误响应需包含、
type、title、status,可选包含detail。errors - 在边界处验证 — 在API层验证传入请求,而非在业务逻辑深处验证。
Patterns
模式
Result Pattern
Result模式
A simple, generic result type that carries either a value or errors.
csharp
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public List<string> Errors { get; }
protected Result(bool isSuccess, List<string>? errors = null)
{
IsSuccess = isSuccess;
Errors = errors ?? [];
}
public static Result Success() => new(true);
public static Result Failure(params string[] errors) => new(false, [..errors]);
public static Result<T> Success<T>(T value) => new(value);
public static Result<T> Failure<T>(params string[] errors) => new(errors);
}
public class Result<T> : Result
{
public T Value { get; }
internal Result(T value) : base(true) => Value = value;
internal Result(IEnumerable<string> errors) : base(false, [..errors]) => Value = default!;
}一种简单的泛型结果类型,可携带值或错误信息。
csharp
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public List<string> Errors { get; }
protected Result(bool isSuccess, List<string>? errors = null)
{
IsSuccess = isSuccess;
Errors = errors ?? [];
}
public static Result Success() => new(true);
public static Result Failure(params string[] errors) => new(false, [..errors]);
public static Result<T> Success<T>(T value) => new(value);
public static Result<T> Failure<T>(params string[] errors) => new(errors);
}
public class Result<T> : Result
{
public T Value { get; }
internal Result(T value) : base(true) => Value = value;
internal Result(IEnumerable<string> errors) : base(false, [..errors]) => Value = default!;
}Result to ProblemDetails Mapping
Result转ProblemDetails映射
csharp
public static class ResultExtensions
{
public static IResult ToProblemDetails(this Result result, int statusCode = 400)
{
return TypedResults.Problem(
title: "One or more errors occurred",
statusCode: statusCode,
extensions: new Dictionary<string, object?>
{
["errors"] = result.Errors
});
}
}
// Usage in endpoint
group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: result.ToProblemDetails();
});csharp
public static class ResultExtensions
{
public static IResult ToProblemDetails(this Result result, int statusCode = 400)
{
return TypedResults.Problem(
title: "One or more errors occurred",
statusCode: statusCode,
extensions: new Dictionary<string, object?>
{
["errors"] = result.Errors
});
}
}
// 端点中的用法
group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: result.ToProblemDetails();
});Global Exception Handler
全局异常处理器
Catches unexpected exceptions and converts them to ProblemDetails. For the modern approach (preferred), see . The inline lambda below works for simple cases:
IExceptionHandlerknowledge/common-infrastructure.mdcsharp
// Program.cs
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception for {Method} {Path}",
context.Request.Method, context.Request.Path);
var problem = new ProblemDetails
{
Title = "An unexpected error occurred",
Status = StatusCodes.Status500InternalServerError,
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1"
};
// Don't leak details in production
if (context.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment())
{
problem.Detail = exception?.Message;
}
context.Response.StatusCode = problem.Status.Value;
await context.Response.WriteAsJsonAsync(problem);
});
});捕获意外异常并转换为ProblemDetails。如需现代方案(推荐),请查看。以下内联lambda适用于简单场景:
IExceptionHandlerknowledge/common-infrastructure.mdcsharp
// Program.cs
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "Unhandled exception for {Method} {Path}",
context.Request.Method, context.Request.Path);
var problem = new ProblemDetails
{
Title = "An unexpected error occurred",
Status = StatusCodes.Status500InternalServerError,
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1"
};
// 生产环境不要泄露细节
if (context.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment())
{
problem.Detail = exception?.Message;
}
context.Response.StatusCode = problem.Status.Value;
await context.Response.WriteAsJsonAsync(problem);
});
});FluentValidation with Endpoint Filters
结合Endpoint Filters的FluentValidation
csharp
// Validator
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty().WithMessage("Customer ID is required");
RuleFor(x => x.Items)
.NotEmpty().WithMessage("At least one item is required");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).NotEmpty();
item.RuleFor(x => x.Quantity).GreaterThan(0);
});
}
}
// Generic validation filter
public class ValidationFilter<TRequest> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices.GetService<IValidator<TRequest>>();
if (validator is null)
return await next(context);
var request = context.Arguments.OfType<TRequest>().FirstOrDefault();
if (request is null)
return await next(context);
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
// Registration
group.MapPost("/", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();csharp
// 验证器
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty().WithMessage("Customer ID is required");
RuleFor(x => x.Items)
.NotEmpty().WithMessage("At least one item is required");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).NotEmpty();
item.RuleFor(x => x.Quantity).GreaterThan(0);
});
}
}
// 通用验证过滤器
public class ValidationFilter<TRequest> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices.GetService<IValidator<TRequest>>();
if (validator is null)
return await next(context);
var request = context.Arguments.OfType<TRequest>().FirstOrDefault();
if (request is null)
return await next(context);
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
// 注册
group.MapPost("/", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();Typed Error Results
类型化错误结果
For richer error handling, use typed error enums or error objects.
csharp
public abstract record Error(string Code, string Message);
public record NotFoundError(string Entity, object Id)
: Error("not_found", $"{Entity} with ID {Id} was not found");
public record ValidationError(string Field, string Message)
: Error("validation", Message);
public record ConflictError(string Message)
: Error("conflict", Message);
// Map to HTTP status codes
public static IResult ToHttpResult(this Error error) => error switch
{
NotFoundError => TypedResults.Problem(title: error.Message, statusCode: 404),
ValidationError => TypedResults.Problem(title: error.Message, statusCode: 400),
ConflictError => TypedResults.Problem(title: error.Message, statusCode: 409),
_ => TypedResults.Problem(title: error.Message, statusCode: 500)
};如需更丰富的错误处理,可使用类型化错误枚举或错误对象。
csharp
public abstract record Error(string Code, string Message);
public record NotFoundError(string Entity, object Id)
: Error("not_found", $"{Entity} with ID {Id} was not found");
public record ValidationError(string Field, string Message)
: Error("validation", Message);
public record ConflictError(string Message)
: Error("conflict", Message);
// 映射到HTTP状态码
public static IResult ToHttpResult(this Error error) => error switch
{
NotFoundError => TypedResults.Problem(title: error.Message, statusCode: 404),
ValidationError => TypedResults.Problem(title: error.Message, statusCode: 400),
ConflictError => TypedResults.Problem(title: error.Message, statusCode: 409),
_ => TypedResults.Problem(title: error.Message, statusCode: 500)
};Anti-patterns
反模式
Don't Throw Exceptions for Flow Control
不要用异常控制流程
csharp
// BAD — exceptions for expected outcomes
public Order GetOrder(Guid id)
{
var order = db.Orders.Find(id)
?? throw new NotFoundException($"Order {id} not found");
return order;
}
// GOOD — Result pattern
public Result<Order> GetOrder(Guid id)
{
var order = db.Orders.Find(id);
return order is not null
? Result.Success(order)
: Result.Failure<Order>($"Order {id} not found");
}csharp
// 错误示例 — 为预期结果抛出异常
public Order GetOrder(Guid id)
{
var order = db.Orders.Find(id)
?? throw new NotFoundException($"Order {id} not found");
return order;
}
// 正确示例 — 使用Result模式
public Result<Order> GetOrder(Guid id)
{
var order = db.Orders.Find(id);
return order is not null
? Result.Success(order)
: Result.Failure<Order>($"Order {id} not found");
}Don't Return Raw Error Strings from APIs
不要从API返回原始错误字符串
csharp
// BAD — inconsistent error format
return Results.BadRequest("Something went wrong");
return Results.BadRequest(new { error = "Invalid input" });
// GOOD — always ProblemDetails
return TypedResults.Problem(title: "Invalid input", statusCode: 400);
return TypedResults.ValidationProblem(validationResult.ToDictionary());csharp
// 错误示例 — 错误格式不一致
return Results.BadRequest("Something went wrong");
return Results.BadRequest(new { error = "Invalid input" });
// 正确示例 — 始终返回ProblemDetails
return TypedResults.Problem(title: "Invalid input", statusCode: 400);
return TypedResults.ValidationProblem(validationResult.ToDictionary());Don't Catch and Swallow Exceptions
不要捕获并吞掉异常
csharp
// BAD — silently swallowing
try { await ProcessOrder(order); }
catch (Exception) { /* ignore */ }
// GOOD — log and handle appropriately
try { await ProcessOrder(order); }
catch (PaymentException ex)
{
logger.LogWarning(ex, "Payment failed for order {OrderId}", order.Id);
return Result.Failure<Order>("Payment processing failed");
}csharp
// 错误示例 — 静默吞掉异常
try { await ProcessOrder(order); }
catch (Exception) { /* ignore */ }
// 正确示例 — 记录日志并妥善处理
try { await ProcessOrder(order); }
catch (PaymentException ex)
{
logger.LogWarning(ex, "Payment failed for order {OrderId}", order.Id);
return Result.Failure<Order>("Payment processing failed");
}Decision Guide
决策指南
| Scenario | Recommendation |
|---|---|
| Expected business failure | Result pattern |
| Input validation | FluentValidation with endpoint filter |
| Unexpected crash | Global exception handler → ProblemDetails |
| API error format | RFC 9457 ProblemDetails — always |
| Validation in handler | Return Result.Failure, don't throw |
| External service failure | Catch specific exception, return Result.Failure |
| Logging errors | Structured logging with correlation ID |
| 场景 | 推荐方案 |
|---|---|
| 预期业务失败 | Result模式 |
| 输入验证 | 结合端点过滤器的FluentValidation |
| 意外崩溃 | 全局异常处理器 → ProblemDetails |
| API错误格式 | 始终遵循RFC 9457 ProblemDetails |
| 处理器内验证 | 返回Result.Failure,不要抛出异常 |
| 外部服务失败 | 捕获特定异常,返回Result.Failure |
| 错误日志记录 | 带关联ID的结构化日志 |