异常处理与响应消息模式设计指南
异常处理与响应消息模式设计指南
在编码过程中,常常有种疑惑,什么时候用Response包裹错误返回,什么时候直接抛异常,对此,我进行了分析。
1 两种机制的定位
维度 | 异常 (Exception) | 响应消息 (ResponseMsg / Result) |
---|---|---|
语义 | 系统级或非预期 的错误;表示当前代码路径已无法继续正常执行业务。 | 业务级或可预期 的结果;包含成功/失败、提示信息、业务数据。 |
控制流 | 通过 CLR 的异常机制跳出当前调用栈;必须 try–catch 。 | 作为方法返回值正常流转;调用方显式检查 IsSuccess 等标志。 |
代价 | 抛出 & 捕获成本高(创建堆栈信息、影响 JIT 内联)。 | 只是一块数据,几乎无额外开销。 |
可测试性 | 单元测试需 Assert.Throws... ;断言粒度粗。 | 断言对象属性;更细粒度。 |
并发/异步 | 异常会通过 Task.Exception 聚合,若未捕获→TaskScheduler.UnobservedTaskException 。 | 结果对象天然兼容 async/await ;无额外陷阱。 |
2 什么时候用异常?
只有当 代码无法恢复 或 调用方不可能预料 时,才应该抛异常。
场景示例
- 系统资源或环境问题
- 文件读写失败(磁盘损坏、权限不足)。
- 数据库连接超时。
- 外部组件/平台调用失败
- COM 组件返回错误。
- HTTP 请求网络级错误(DNS 解析失败)。
- 编程错误(Bug)
- 空引用、越界、格式错误。
- 服务端内部严重故障
- 依赖服务挂掉。
建议
- 抛异常时立即捕获并记录日志(NLog)再向上抛或包装为业务错误。
- 队列消费者中的异常应
catch
,避免吞掉线程;用Dispatcher.Invoke
把提示回 UI。
/// <summary>读取配置文件,无法读取时抛自定义异常</summary>
public static Config LoadConfig(string path)
{
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Config>(json)!;
}
catch (IOException ex)
{
_logger.Error(ex, "配置文件读取失败");
throw new ConfigLoadException("配置文件读取失败,请检查磁盘或权限", ex);
}
}
3 什么时候用 ResponseMsg / Result?
当 错误是业务层可预测且希望继续流程 时,用响应消息更合适。
场景示例
- 表单校验
- 字段必填、格式、范围检测——告诉用户“手机号格式不正确”。
- 权限/流程判断
- “用户余额不足”“工单已关闭,不能再审批”。
- 幂等或软失败
- “记录已存在”返回 Code=409,但整体流程不崩溃。
- 后台任务结果
- 队列消费完毕后,需要返回执行状态 & 消息。
典型定义
/// <summary>统一业务结果包装</summary>
public sealed record ResponseMsg<T>
{
public bool IsSuccess { get; init; }
public string? Message { get; init; }
public T? Data { get; init; }
public static ResponseMsg<T> Ok(T data, string? msg = null) =>
new() { IsSuccess = true, Data = data, Message = msg };
public static ResponseMsg<T> Fail(string msg) =>
new() { IsSuccess = false, Message = msg };
}
调用方示例:
var result = await _orderService.PlaceAsync(request);
if (!result.IsSuccess)
{
_uiNotifier.Warn(result.Message);
return;
}
UpdateUI(result.Data);
4 在全局多线程队列中的融合做法
4.1 消息体区分系统异常与业务结果
public record ToolExecuteResultMessage : QueueMessage
{
public ResponseMsg<object?> Result { get; init; } = ResponseMsg<object?>.Ok(null);
public Exception? Exception { get; init; }
}
- 消费者:内部
try–catch
;捕获系统异常填Exception
;业务失败设置Result.IsSuccess=false
。 - UI 线程:
Exception
不为空 → 显示“系统错误”并写日志。- 否则根据
Result.IsSuccess
决定提示样式。
4.2 最佳实践
规则 | 说明 |
---|---|
宁可多返回 ResponseMsg,也不要滥抛异常 | 业务流转更清晰,可组合。 |
所有线程边界都要 catch | 包括 Task.Run 、队列循环;绝不让异常溢出到线程池。 |
异常要带上下文 | 自定义异常加属性,如 ToolId 、StepName ,方便排障。 |
错误日志五要素 | 时间、级别、位置、上下文、堆栈。NLog 可配置。 |
5 结合 Unit Test 的对比
// 断言异常
Assert.ThrowsAsync<ConfigLoadException>(() => _service.LoadConfigAsync("bad.json"));
// 断言业务失败
var res = await _orderService.PlaceAsync(invalidDto);
Assert.False(res.IsSuccess);
Assert.Equal("手机号格式不正确", res.Message);
- 异常测试:通常只验证类型。
- 响应对象测试:可验证业务语义(Message/Data)。
6 小结
- 用异常处理不可预期/系统级错误,让程序“快失败”并留痕。
- 用 ResponseMsg 表达业务可预期的成功与失败,让调用链保持顺畅。
- 在多线程/队列环境:
- 消费者内部 捕获异常并包装后丢入结果消息,避免线程终止。
- UI 层 根据
Exception
或Result.IsSuccess
做区分化提示。
- 配合 NLog、Autofac、CommunityToolkit.Mvvm,可实现易排查、可扩展、线程安全的错误处理体系。
主题测试文章,只做测试使用。发布者:admin,转转请注明出处:http://onebyone.icu/2025/06/05/%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86%e4%b8%8e%e5%93%8d%e5%ba%94%e6%b6%88%e6%81%af%e6%a8%a1%e5%bc%8f%e8%ae%be%e8%ae%a1%e6%8c%87%e5%8d%97/