2.7 异常处理与结果对象
异常适合表达不可预期或无法在当前层处理的问题,业务失败适合用明确结果对象表达。两者混用会导致 API 错误不稳定、测试难写、日志噪音过多。
学习目标
- 能区分异常和业务失败。
- 能设计简单结果对象表达成功和失败。
- 能在合适层捕获异常并记录日志。
- 能避免吞异常和过度使用异常控制流程。
应用场景
- 创建任务时标题为空是业务校验失败。
- 数据库连接失败是基础设施异常。
- 完成不存在的任务是可预期失败。
- 外部服务超时需要记录并返回稳定错误。
核心概念
| 类型 | 说明 | 示例 |
|---|---|---|
| 业务失败 | 输入或状态不满足规则 | 标题为空、任务不存在 |
| 异常 | 非预期或无法当前处理 | 数据库不可用、文件损坏 |
| 结果对象 | 明确返回成功或失败 | Result<T>、枚举结果 |
案例:完成任务结果
public enum CompleteTodoStatus
{
Completed,
NotFound,
AlreadyCompleted,
}
public sealed class CompleteTodoHandler(ITodoRepository repository)
{
public async Task<CompleteTodoStatus> HandleAsync(
int id,
CancellationToken cancellationToken)
{
var todo = await repository.FindByIdAsync(id, cancellationToken);
if (todo is null)
{
return CompleteTodoStatus.NotFound;
}
if (todo.IsCompleted)
{
return CompleteTodoStatus.AlreadyCompleted;
}
todo.Complete();
await repository.SaveChangesAsync(cancellationToken);
return CompleteTodoStatus.Completed;
}
}
API 映射示例
app.MapPut("/todos/{id:int}/complete", async (
int id,
CompleteTodoHandler handler,
CancellationToken cancellationToken) =>
{
var status = await handler.HandleAsync(id, cancellationToken);
return status switch
{
CompleteTodoStatus.Completed => Results.NoContent(),
CompleteTodoStatus.AlreadyCompleted => Results.NoContent(),
CompleteTodoStatus.NotFound => Results.NotFound(new {message = "任务不存在"}),
_ => Results.Problem("未知任务状态"),
};
});
重点难点
- 不要用异常表达正常业务分支,例如“没查到数据”。
- 不要捕获异常后什么也不做,这会隐藏真实问题。
- 统一异常处理中记录日志,API 返回稳定错误契约。
- 业务结果对象要足够简单,避免过早设计复杂通用框架。
常见误区
| 误区 | 推荐做法 |
|---|---|
所有失败都 throw | 可预期业务失败返回结果对象 |
| 捕获异常后返回默认值 | 记录日志并返回明确失败 |
| 每层都捕获同一个异常 | 在能处理的边界捕获,其他层继续抛出 |
练习
- 为创建任务设计
CreateTodoResult。 - 把“任务不存在”从异常改成结果枚举。
- 在统一异常处理中添加
traceId。
延伸阅读
- C# 异常处理。
- Problem Details。
- Result Pattern 与业务错误建模。