早抛晚捕:异常处理

早抛晚捕:异常处理

早抛晚捕

“早抛晚捕”(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) {
// 1. 【早抛】第一时间检查输入
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("转账金额必须大于0");
}

// 2. 【早抛】第一时间检查业务前置条件
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
# service.py
def withdraw_money(user_id, amount):
user = User.query.get(user_id)
# 如果 user 是 None,下一行直接报 AttributeError: 'NoneType' object has no attribute 'balance'
user.balance -= amount
db.session.commit()

# app.py
@app.route('/withdraw', methods=['POST'])
def withdraw():
data = request.json
# 如果 amount 是字符串或缺失,这里直接崩
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
# service.py
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.py
@app.route('/withdraw', methods=['POST'])
def withdraw():
data = request.json
# 痛苦的开始:必须判断 Service 的各种返回结果
res = withdraw_money(data.get('user_id'), data.get('amount'))

if res['code'] != 200:
# 每个 API 都要写这段重复的判断逻辑
return jsonify({"error": res['err']}), res['code']

return jsonify({"message": res['msg']}), 200
  • 后果:
    1. 代码膨胀:40 个 API 你要写 40 次 if res['code'] != 200
    2. 职责混乱:Service 层竟然在操心 HTTP 状态码(404, 400)。
    3. 极难维护:如果哪天想把 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
# --- 1. 定义异常 (早抛的工具) ---
class BusinessException(Exception):
def __init__(self, message, status_code=400):
self.message = message
self.status_code = status_code

# --- 2. 服务层 (早抛 Throw Early) ---
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()

# --- 3. 视图层 (简洁明了) ---
@app.route('/withdraw', methods=['POST'])
def withdraw():
data = request.json
# 像写诗一样简洁:没有任何 try-catch 和 if 判断
withdraw_money(data.get('user_id'), data.get('amount'))
return jsonify({"message": "提现成功"}), 200

# --- 4. 全局捕获器 (晚捕 Catch Late) ---
@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

为什么第三种更好?

  1. Service 层变得极其纯粹:它只负责业务逻辑,报错时直接 raise,符合人类直觉(“错了就喊出来”)。
  2. Controller 层零负担:你的 40 个 API 函数都会缩减到只有 2-3 行,大大降低了阅读压力。
  3. 一致性保证:无论哪个 API 报错,返回给前端的 JSON 结构(如 {"status": "fail", "message": "..."})永远是一样的。
  4. 方便集成大模型:如果以后加入 AI 校验,校验失败只需 raise AIValidationError("AI觉得你这操作不对"),现有的全局捕获器会立即接管,无需改动任何 API 路由代码。