直接使用内置异常类(如valueerror、runtimeerror)会导致业务语义缺失、日志监控混乱、捕获逻辑耦合、难以分层处理及跨模块统一管理,故大型项目必须定义带业务语义的自定义异常体系。

为什么不能直接用内置异常类
大型项目里直接抛 ValueError 或 RuntimeError 看似省事,实际会快速失控:业务逻辑错误、外部服务超时、配置缺失混在一起,日志里全是泛型异常,监控无法区分严重等级,调用方也难做针对性重试或降级。更麻烦的是,不同模块各自定义异常名(比如 UserError 和 UserNotFoundError),后期想统一捕获反而得写一长串 except。
- 异常类型不承载业务语义,
except ValueError 可能拦下参数校验失败,也可能拦下数据库字段转换失败,二者处理方式完全不同
- 无法按层级做宽泛捕获(如所有“用户域错误”统一记录审计日志)
- IDE 和静态检查工具难以识别自定义业务约束,比如“该函数只应抛出
UserPermissionError”,而不会抛 ConnectionError
如何设计可扩展的异常基类树
核心是两层继承:顶层一个项目级基类,向下按**领域**(非技术栈)分叉,再按**错误动因**细化。不要按 HTTP 状态码、日志级别或框架分(如 Flask vs FastAPI),那些是下游适配逻辑,不该污染异常定义本身。
- 所有异常必须继承自统一基类,例如
AppError,且该类不继承任何内置异常(避免意外触发默认行为)
- 领域子类命名带明确前缀,如
UserError、PaymentError、InventoryError,禁止出现 ApiError 这种模糊范畴
- 具体异常名体现失败动作和原因,如
UserNotFoundError、UserEmailConflictError,而非 InvalidUserError(太笼统)
- 每个异常类应预设
status_code(仅用于 API 层映射)、error_code(唯一字符串 ID,如 "USER_NOT_FOUND"),但不强制要求构造时传参
class AppError(Exception):
status_code = 500
error_code = "INTERNAL_ERROR"
<p>class UserError(AppError):
status_code = 400</p><p>class UserNotFoundError(UserError):
status_code = 404
error_code = "USER_NOT_FOUND"</p>
在各层中如何正确抛出和捕获
抛异常不是越早越好,关键看谁该负责处理它。常见误操作是 DAO 层直接抛 UserNotFoundError,但该异常本意是业务语义丢失——DAO 只知道“查不到记录”,并不知道这是否构成“用户不存在”的业务失败(可能只是缓存穿透)。应由 Service 层根据上下文决定异常类型。
- Repository/DAO 层只抛底层技术异常,如
DatabaseConnectionError、RedisTimeoutError,且必须是项目内定义的、带 AppError 血统的类
- Service 层接收技术异常后,根据用例目标决定是否转译:查用户详情没找到 →
UserNotFoundError;但查用户统计聚合没数据 → 可能是正常空结果,不抛异常
- API 层统一拦截
AppError 子类,提取 error_code 和 status_code 构造响应,其他未捕获异常走兜底 500
- 调用方应按领域类捕获,而不是单个具体异常:
except UserError: 处理所有用户相关失败,留出扩展空间
容易被忽略的兼容性与演进细节
异常类不是写完就封存的。随着业务增长,你会遇到:新增子类要不要加到已有 except 块?老异常要不要改 error_code?这些看似小问题,实际影响线上稳定性。
- 所有
error_code 必须全局唯一且永不变更,哪怕对应异常类被废弃,其 code 也不能复用(防止日志分析错乱)
- 新增子类时,确保父类
except 仍能捕获它——Python 的 MRO 是可靠的,但要注意别意外覆盖 <strong>init</strong> 导致父类初始化失败
- 不要为减少
except 分支而滥用 BaseException 或裸 except:,它们会吞掉 KeyboardInterrupt 和 SystemExit
- 如果项目跨语言(如 Python 服务调用 Go 微服务),
error_code 字符串需与对方约定一致,避免两边各自编码导致对不上
except ValueError 可能拦下参数校验失败,也可能拦下数据库字段转换失败,二者处理方式完全不同 UserPermissionError”,而不会抛 ConnectionError
- 所有异常必须继承自统一基类,例如
AppError,且该类不继承任何内置异常(避免意外触发默认行为) - 领域子类命名带明确前缀,如
UserError、PaymentError、InventoryError,禁止出现ApiError这种模糊范畴 - 具体异常名体现失败动作和原因,如
UserNotFoundError、UserEmailConflictError,而非InvalidUserError(太笼统) - 每个异常类应预设
status_code(仅用于 API 层映射)、error_code(唯一字符串 ID,如"USER_NOT_FOUND"),但不强制要求构造时传参
class AppError(Exception):
status_code = 500
error_code = "INTERNAL_ERROR"
<p>class UserError(AppError):
status_code = 400</p><p>class UserNotFoundError(UserError):
status_code = 404
error_code = "USER_NOT_FOUND"</p>
在各层中如何正确抛出和捕获
抛异常不是越早越好,关键看谁该负责处理它。常见误操作是 DAO 层直接抛 UserNotFoundError,但该异常本意是业务语义丢失——DAO 只知道“查不到记录”,并不知道这是否构成“用户不存在”的业务失败(可能只是缓存穿透)。应由 Service 层根据上下文决定异常类型。
- Repository/DAO 层只抛底层技术异常,如
DatabaseConnectionError、RedisTimeoutError,且必须是项目内定义的、带 AppError 血统的类
- Service 层接收技术异常后,根据用例目标决定是否转译:查用户详情没找到 →
UserNotFoundError;但查用户统计聚合没数据 → 可能是正常空结果,不抛异常
- API 层统一拦截
AppError 子类,提取 error_code 和 status_code 构造响应,其他未捕获异常走兜底 500
- 调用方应按领域类捕获,而不是单个具体异常:
except UserError: 处理所有用户相关失败,留出扩展空间
容易被忽略的兼容性与演进细节
异常类不是写完就封存的。随着业务增长,你会遇到:新增子类要不要加到已有 except 块?老异常要不要改 error_code?这些看似小问题,实际影响线上稳定性。
- 所有
error_code 必须全局唯一且永不变更,哪怕对应异常类被废弃,其 code 也不能复用(防止日志分析错乱)
- 新增子类时,确保父类
except 仍能捕获它——Python 的 MRO 是可靠的,但要注意别意外覆盖 <strong>init</strong> 导致父类初始化失败
- 不要为减少
except 分支而滥用 BaseException 或裸 except:,它们会吞掉 KeyboardInterrupt 和 SystemExit
- 如果项目跨语言(如 Python 服务调用 Go 微服务),
error_code 字符串需与对方约定一致,避免两边各自编码导致对不上
DatabaseConnectionError、RedisTimeoutError,且必须是项目内定义的、带 AppError 血统的类 UserNotFoundError;但查用户统计聚合没数据 → 可能是正常空结果,不抛异常 AppError 子类,提取 error_code 和 status_code 构造响应,其他未捕获异常走兜底 500 except UserError: 处理所有用户相关失败,留出扩展空间 except 块?老异常要不要改 error_code?这些看似小问题,实际影响线上稳定性。
- 所有
error_code必须全局唯一且永不变更,哪怕对应异常类被废弃,其 code 也不能复用(防止日志分析错乱) - 新增子类时,确保父类
except仍能捕获它——Python 的 MRO 是可靠的,但要注意别意外覆盖<strong>init</strong>导致父类初始化失败 - 不要为减少
except分支而滥用BaseException或裸except:,它们会吞掉KeyboardInterrupt和SystemExit - 如果项目跨语言(如 Python 服务调用 Go 微服务),
error_code字符串需与对方约定一致,避免两边各自编码导致对不上
异常层次不是越深越好,真正重要的是每个节点都有不可替代的业务解释力。当新人看到 InventoryInsufficientStockError 就能立刻明白发生了什么、该查库存服务还是前端传参,这个设计才算落地。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











