2. 错误处理基础:异常与错误的区别、错误码设计原则、错误信息分类
在构建企业级业务系统时,错误处理并非简单的“try-catch”堆砌,而是一套贯穿开发与生产环境的语义体系。本章从三个核心维度建立共识:异常与错误的本质区别、错误码的设计原则以及错误信息的分类标准。只有打好这个基础,后续的日志治理、告警收敛、环境适配才有统一的语言。
2.1 异常与错误的本质区别
很多团队将“异常”和“错误”混用,但在架构层面,二者有明确的职责边界。下表从触发源、可恢复性、处理策略三个维度进行对比:
| 对比维度 | 异常(Exception) | 错误(Error) |
|---|---|---|
| 触发源 | 业务逻辑违规、资源暂时不可用、参数校验失败 | 系统级故障、硬件失效、内存溢出、栈溢出 |
| 可恢复性 | 通常可恢复(重试、降级、补偿) | 通常不可恢复,需人工介入或重启进程 |
| 处理策略 | 调用方主动捕获并决策(重试/回滚/返回降级结果) | 记录现场快照后快速失败,避免数据污染 |
| 开发环境关注点 | 堆栈可读性、断点命中、参数上下文 | 内存转储、线程快照、GC日志 |
| 生产环境关注点 | 错误码、链路追踪、业务影响面 | 告警通知、自动重启、健康检查 |
架构原则:异常是业务契约的一部分,错误是基础设施的边界。在代码中,
Exception 应被设计为可预见、可分类、可处理;Error 应被设计为不可静默吞噬,必须向上传播至全局处理器。
以下 SVG 展示了异常与错误在调用链中的不同传播路径与处理节点:
2.2 错误码设计原则
错误码是开发与生产环境之间最直接的“翻译器”。一套好的错误码体系,能让开发者在日志中秒级定位问题,也能让监控系统自动聚合同类故障。以下是经过多个企业级项目验证的六条核心原则:
- 唯一性:每个错误码全局唯一,不重复、不重载。禁止同一个错误码在不同模块表示不同含义。
- 可扩展性:编码结构预留模块位和版本位,避免因新增业务场景导致整体重构。
- 可读性:错误码本身携带分类信息,例如
PAY-4001表示支付模块的客户端参数错误。 - 稳定性:已发布的错误码不可删除、不可变更含义;废弃码仅标记为“已弃用”,保留占位。
- 层级化:错误码按“系统级 → 模块级 → 业务级”分层定义,不同层级由不同团队管理。
- 环境透明:错误码在开发、测试、预发、生产环境保持一致,不因环境不同而改变含义。
推荐编码结构:
{模块前缀}-{HTTP状态码简写}{错误序号},例如 ORD-4001(订单模块,400 类参数错误,序号 01)。这种结构在日志聚合时可直接按前缀和状态码分类,无需额外解析。
以下表格展示了一个典型的企业级错误码分级示例:
| 层级 | 错误码范围 | 示例 | 管理方 |
|---|---|---|---|
| 系统级 | SYS-0001 ~ SYS-9999 | SYS-0500 内部服务不可用 |
基础架构团队 |
| 模块级 | PAY-0001 ~ PAY-9999 | PAY-4003 支付金额超限 |
支付团队 |
| 业务级 | ORD-0001 ~ ORD-9999 | ORD-4001 订单商品库存不足 |
订单业务团队 |
| 第三方集成级 | THD-0001 ~ THD-9999 | THD-5021 物流网关超时 |
集成对接团队 |
2.3 错误信息分类
错误信息不能只有堆栈和错误码,还需要按用途分类,以便不同角色(开发者、运维、客服、终端用户)都能获取到有效信息。我们将错误信息分为以下四类:
- 技术诊断信息:面向开发者和运维,包含完整堆栈、线程 ID、请求追踪 ID、数据库连接状态等。仅在开发环境和生产环境的调试白名单中输出。
- 业务上下文信息:面向业务开发人员,包含当前请求参数、用户 ID、订单号、触发规则等。用于快速复现业务异常场景。
- 用户友好信息:面向终端用户,使用自然语言描述,不暴露技术细节。例如“当前服务繁忙,请稍后重试”。
- 审计合规信息:面向合规与安全团队,包含操作时间、操作人、操作类型、数据变更前后快照。不可被用户或普通开发者查看。
以下 SVG 展示了四类错误信息在开发与生产环境中的输出策略差异:
2.4 代码示例:错误码与信息分类的落地实现
以下 Java 代码片段展示了如何将错误码、异常分类与信息分层结合,实现一个可适配开发与生产环境的错误处理骨架:
// 错误码枚举,携带模块前缀、HTTP状态码、用户友好信息
public enum ErrorCode {
ORD_4001("ORD-4001", 400, "订单商品库存不足", "当前商品库存不足,请调整购买数量"),
PAY_5002("PAY-5002", 500, "支付网关超时", "支付服务暂时不可用,请稍后重试"),
SYS_0500("SYS-0500", 500, "内部服务不可用", "系统繁忙,请稍后再试");
private final String code;
private final int httpStatus;
private final String techMessage; // 技术诊断信息
private final String userMessage; // 用户友好信息
// 构造方法、getter 省略
}
// 业务异常,携带错误码和业务上下文
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> context; // 业务上下文信息
public BusinessException(ErrorCode errorCode, Map<String, Object> context) {
super(errorCode.getTechMessage());
this.errorCode = errorCode;
this.context = context;
}
// 根据环境输出不同信息
public Object toResponse(boolean isDebugEnabled) {
Map<String, Object> result = new HashMap<>();
result.put("code", errorCode.getCode());
result.put("message", errorCode.getUserMessage());
if (isDebugEnabled) {
result.put("debug", context); // 仅开发/调试环境输出
result.put("stack", this.getStackTrace());
}
return result;
}
}
生产环境陷阱:切勿在生产环境将
context 或 stack 直接序列化到 HTTP 响应中。这会导致敏感数据泄露(如用户手机号、支付密码)和响应体过大。始终通过环境标志或白名单机制控制技术信息的输出。