刚入行那会儿,我写 Java 代码有个“强迫症”:
每个方法都包一层 try-catch,生怕程序崩。
public User getUserById(Long id) {
try {
return userMapper.selectById(id);
} catch (Exception e) {
log.error("查询用户失败", e);
return null;
}
}
看起来很负责?其实这是在给系统埋雷。
直到去年一个线上事故,我才彻底醒悟:乱用 try-catch,比不处理异常更危险。
事故现场:一个 null 引发的数据错乱
我们有个订单导出功能,逻辑大概是:
- 根据订单 ID 查用户;
- 拼装 Excel 行数据;
- 写入文件。
因为 getUserById 在异常时返回 null,而下游代码没判空,直接调用了 user.getName() —— 结果 NPE,整个导出任务中断。
更要命的是,因为异常被“吃掉”了,日志里只有一行模糊的 “查询用户失败”,根本不知道是哪个订单、什么参数导致的。运维翻了半小时日志才定位到问题。
客户那边,几百个商户的对账单全卡住,差点终止合作。
为什么“处处 try-catch”是个坏习惯?
1. 掩盖真实问题
异常是系统的“警报器”。你把它 catch 了又不做有效处理(比如返回 null 或空字符串),等于把警报关了,但火还在烧。
2. 破坏调用链的语义
上游调用者以为方法执行成功了(毕竟没抛异常),结果拿到一个 null,后续逻辑全乱套。这比直接抛异常更难排查。
3. 让监控和告警失效
如果你用 APM(比如 SkyWalking、Arthas)或 ELK 做异常监控,被吞掉的异常根本不会上报。等你发现时,可能已经影响成千上万用户。
那到底该怎么处理异常?
我的原则就一条:只在你能真正“处理”异常的地方 catch 它。
什么叫“能处理”?举几个例子:
- 重试:网络超时,可以再试一次;
- 降级:查缓存失败,走数据库兜底;
- 用户友好提示:比如“手机号格式错误”,而不是“500 Internal Error”。
除此之外,别 catch,让它往上抛!
✅ 正确姿势 1:让 Controller 统一兜底
Spring Boot 里,用 @ControllerAdvice 全局处理异常,既干净又可控:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
return ResponseEntity.status(404).body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
log.error("系统异常", e); // 这里才该打 error 日志!
return ResponseEntity.status(500).body(new ErrorResponse("服务暂时不可用"));
}
}
这样,Service 层完全不用写 try-catch,专注业务逻辑。出问题时,日志清晰、响应规范、监控也能抓到。
✅ 正确姿势 2:明确声明 checked exception(如果真有必要)
比如读取配置文件失败,你可以自定义一个 ConfigLoadException,并在方法签名 throws 出去。调用方必须面对它,而不是假装没事发生。
✅ 正确姿势 3:记录上下文,别只 log.error(e)
如果真要在中间层记录日志(比如重要业务入口),一定要带上关键参数:
try {
processOrder(orderId);
} catch (Exception e) {
log.error("处理订单失败, orderId={}", orderId, e); // 👈 关键!
throw e; // 别吞掉,继续抛
}
这样排查时,直接搜 orderId 就能找到完整链路。
特别提醒:别用 Exception 接所有异常!
见过太多人这么写:
catch (Exception e) { ... }
这会把 NullPointerException、IllegalArgumentException 这类本该快速失败的编程错误也吞掉。
正确的做法是:
- 能处理的特定异常,单独 catch;
- 不能处理的,让它们自然抛出;
- 如果非要兜底,至少用
RuntimeException,别用Exception。
总结:少即是多
删掉那些无意义的 try-catch 后,我们的代码:
- 更简洁(Service 层清爽多了);
- 更可靠(异常不再静默消失);
- 更好排查(日志+监控联动,5 分钟定位问题)。
作为不想打工的码农,我们接项目、做外包,最怕的就是半夜被叫起来修“莫名其妙”的 bug。
而很多“莫名其妙”,其实都是当初为了“省事”埋下的。
真正的健壮,不是不让程序出错,而是让错误暴露得足够快、足够清楚。