早抛晚捕:异常处理
早抛晚捕
“早抛晚捕”(Throw Early, Catch Late)是异常处理中非常经典的设计原则。它的核心思想是:在错误发生的第一时间发现并抛出异常,而将异常的处理推迟到有足够上下文(Context)来决定如何应对的层面。
下面通过一个“银行转账”的典型场景来详细拆解这个原则。
场景设定
我们要实现一个转账功能,逻辑涉及三层: 1. Controller(接口层):负责接收用户请求并展示结果。 2. Service(业务层):负责具体的转账逻辑。 3. Repository(数据层):负责数据库的读写。
早抛(Throw Early):在源头拦截错误
“早抛”指的是:一旦发现参数不合法或状态不符合预期,立即抛出异常,不要让错误的代码继续往下执行。
反面教材:
如果不“早抛”,代码可能会带着错误的数据进入深层逻辑,最后抛出一个莫名其妙的 NullPointerException 或导致数据损坏。
正确示范(业务层):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void transfer(Long fromId, Long toId, BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("转账金额必须大于0"); }
Account fromAccount = accountRepo.findById(fromId); if (fromAccount == null) { throw new AccountNotFoundException("扣款账户不存在"); }
if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientBalanceException("余额不足"); }
}
|
为什么要早抛?
- 防止污染: 避免无效数据进入复杂的业务逻辑或数据库。
- 精确定位: 报错信息非常直观(如“余额不足”),而不是等到数据库报错。
晚捕(Catch Late):在有决策权的地方处理
“晚捕”指的是:中间层(如 Service 层)不要轻易拦截异常。除非你能在这个位置彻底解决问题,否则应该让异常向上冒泡。
反面教材:
在 Service 层为了图省事写 try-catch 然后 return null。
1 2 3 4 5 6 7 8
| public void transfer(...) { try { } catch (Exception e) { System.out.println("发生错误了"); } }
|
正确示范(全局异常处理器):
异常一直向上抛,直到 全局拦截器 才捕获。因为只有到了这一层,系统才知道如何跟用户交流(是返回 JSON 报错,还是跳转到错误页面)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientBalanceException.class) public Response handleBalanceError(InsufficientBalanceException e) { return Response.error(400, e.getMessage()); }
@ExceptionHandler(Exception.class) public Response handleGenericError(Exception e) { log.error("系统崩溃了", e); return Response.error(500, "服务器开小差了,请稍后再试"); } }
|
为什么要晚捕? * 集中处理: 避免在每个方法里都写重复的 try-catch。 * 职责分明: 底层只负责报告问题,高层负责解决问题(报错、重试或回滚)。
总结:这个原则解决的痛点
| 早抛 (Throw Early) |
保证程序的健壮性。在执行危险操作前,先验证环境。 |
避免由于错误数据引发的连锁反应,让 Bug 容易调试。 |
| 晚捕 (Catch Late) |
保证代码的简洁性和一致性。 |
避免“异常被吞”导致找不到故障原因,也避免了代码中到处是冗余的捕获逻辑。 |
一句话总结:“发现苗头不对马上报(早抛),不归你管别瞎拦(晚捕)。”
三种异常处理方式对比
为了直观感受到重构前后的巨大差异,我们以一个简单的“用户提现”功能为例(涉及参数验证、余额检查、数据库保存)。
场景:用户提现 API
输入:user_id, amount
示例 1:裸奔模式(完全没有异常处理)
表现: 代码看起来最少,但极其脆弱。只要用户不存在、金额不是数字或余额不足,程序直接崩溃,返回 Flask 默认的 HTML 500 错误页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def withdraw_money(user_id, amount): user = User.query.get(user_id) user.balance -= amount db.session.commit()
@app.route('/withdraw', methods=['POST']) def withdraw(): data = request.json withdraw_money(data['user_id'], data['amount']) return {"message": "成功"}, 200
|
- 后果: 前端收到一个巨大的 500 HTML 报错(甚至暴露代码路径),用户体验极差,后台日志里全是 Python 堆栈。
示例 2:防御式散装模式(内部 try-catch,导致冗余混乱)
表现: 每一个 Service 都试图自己处理异常,并返回“错误信息+状态码”。Controller 层必须通过大量的 if 判断来分流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def withdraw_money(user_id, amount): try: user = User.query.get(user_id) if not user: return {"err": "用户不存在", "code": 404} if user.balance < amount: return {"err": "钱不够", "code": 400} user.balance -= amount db.session.commit() return {"msg": "成功", "code": 200} except Exception as e: db.session.rollback() return {"err": str(e), "code": 500}
@app.route('/withdraw', methods=['POST']) def withdraw(): data = request.json res = withdraw_money(data.get('user_id'), data.get('amount')) if res['code'] != 200: return jsonify({"error": res['err']}), res['code'] return jsonify({"message": res['msg']}), 200
|
- 后果:
- 代码膨胀:40 个 API 你要写 40 次
if res['code'] != 200。
- 职责混乱:Service 层竟然在操心 HTTP 状态码(404, 400)。
- 极难维护:如果哪天想把
err 改成 error_msg,你需要全局搜索替换几百处。
示例 3:早抛晚捕模式(标准、整洁、解耦)
表现: Service 只管检查和抛出,Controller 只有一行逻辑,全局捕获器统一负责格式化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class BusinessException(Exception): def __init__(self, message, status_code=400): self.message = message self.status_code = status_code
def withdraw_money(user_id, amount): if amount is None or amount <= 0: raise BusinessException("提现金额不合法") user = User.query.get(user_id) if not user: raise BusinessException("找不到该用户", 404) if user.balance < amount: raise BusinessException("余额不足")
user.balance -= amount db.session.commit()
@app.route('/withdraw', methods=['POST']) def withdraw(): data = request.json withdraw_money(data.get('user_id'), data.get('amount')) return jsonify({"message": "提现成功"}), 200
@app.errorhandler(BusinessException) def handle_business_error(e): return jsonify({"status": "fail", "message": e.message}), e.status_code
@app.errorhandler(Exception) def handle_system_error(e): app.logger.error(f"系统故障: {e}") return jsonify({"status": "error", "message": "服务器冒烟了"}), 500
|
为什么第三种更好?
- Service 层变得极其纯粹:它只负责业务逻辑,报错时直接
raise,符合人类直觉(“错了就喊出来”)。
- Controller 层零负担:你的 40 个 API 函数都会缩减到只有 2-3 行,大大降低了阅读压力。
- 一致性保证:无论哪个 API 报错,返回给前端的 JSON 结构(如
{"status": "fail", "message": "..."})永远是一样的。
- 方便集成大模型:如果以后加入 AI 校验,校验失败只需
raise AIValidationError("AI觉得你这操作不对"),现有的全局捕获器会立即接管,无需改动任何 API 路由代码。