用 try/catch 做 if-else 会导致逻辑脆弱、调试困难、性能骤降、日志失真、静态分析报错;应改用返回值(如 optional、result、error)显式表达业务分支。

用 try/catch 做 if-else 会出什么问题
这不是语法错误,但会让逻辑变得脆弱、难调试、难维护。异常机制的设计目标是处理「意外」,不是「预期中的业务分支」。一旦把 throw 当成 goto 用,调用栈就失去意义,堆栈信息里全是业务判断点,而不是真正的错误现场。
- 日志里满屏
BusinessRuleException,根本分不清是用户输错密码,还是数据库挂了 - JVM/CLR 会对异常路径做特殊优化(如跳过某些 JIT 编译),频繁抛 catch 会导致性能骤降——哪怕没打印堆栈,开销也比普通分支高 10–100 倍
- 静态分析工具(如 SonarQube、ESLint)会直接标红:「Avoid using exceptions for control flow」
典型坏习惯代码长什么样
比如用户注册时,检查手机号是否已存在,有人写成这样:
try {
userRepo.findById(phone);
throw new BusinessException("手机号已被注册");
} catch (EmptyResultDataAccessException e) {
// 正常流程,继续注册
userRepo.save(new User(phone, ...));
}
这本质是把「查无结果」这个完全可预期的返回值,硬塞进异常通道。JPA 的 findById 返回 Optional 或 null,本就该用 .isPresent() 判断。
- 错误根源:混淆了「资源未找到」(
404级别)和「系统故障」(500级别) - 更糟的是,有些框架(如 Spring Data JPA)在配置
spring.jpa.properties.hibernate.jdbc.batch_size=0时,EmptyResultDataAccessException还可能被静默吞掉 - 测试时容易漏掉异常分支覆盖,因为要 mock 数据库返回空,而正常流程反而更容易测
什么时候真该用异常控制流程
极少,但存在。只适用于:无法通过返回值表达语义,且该情况属于「当前层无法决策,必须交由上层统一兜底」的场景。
- 权限校验失败:
SecurityException不该在 service 层 try-catch,而应由全局@ControllerAdvice拦截并转成403 - 第三方服务不可用:调用支付网关超时,抛
PaymentGatewayTimeoutException,让重试切流组件捕获,而不是在订单 service 里写 if (result == null) {...} - 注意:
IllegalArgumentException和IllegalStateException是合法的防御性编程手段,但它们属于「输入校验失败」,不是业务规则分支
替换方案怎么写才干净
核心原则:把分支逻辑留在能理解业务语义的层级,用类型或状态码显式表达。
- Java 可用
Result<t></t>封装(如 Vavr 的Try或自定义Response<t></t>),让userRepo.checkPhoneAvailable(phone)直接返回Response.success()或Response.fail("already_exists") - Go 用
func (r *Repo) FindUserByPhone(phone string) (*User, error),约定error == nil表示存在,errors.Is(err, ErrNotFound)表示不存在——这是 Go 的惯用法,不违背原则 - HTTP API 层统一用
409 Conflict表示「手机号已存在」,而不是内部抛异常再转成409;前端看到409就知道该提示用户换号,不用猜后端发生了什么
最麻烦的其实是团队认知——当所有人都习惯在 DAO 层 throw,修复就得从最底层开始改起,中间每层都要同步调整返回类型。这种债,越拖越重。










