跳到主要内容

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 与业务错误建模。