异常链需显式构造,必须用带Throwable cause参数的构造函数传递原始异常,否则上下文丢失;自定义异常须提供双参构造器;日志中出现“Caused by:”表明链生效。

异常链不是自动发生的,必须显式构造
Java **不会自动把捕获到的异常“塞进”新异常里**。你写 throw new BusinessException("下单失败"),原始的 SQLException 或 TimeoutException 就彻底丢了——堆栈里只剩一句空洞描述,日志中也不会出现 Caused by:。真正起作用的,是你主动把旧异常作为 cause 传进去。
- ✅ 正确做法:用带
Throwable cause参数的构造函数,如new BusinessException("库存扣减失败", e) - ❌ 错误做法:只取
e.getMessage()构造新异常,或漏掉e直接抛新异常 - ⚠️ 注意:
cause本身也可以有getCause(),但链深超过 2~3 层反而难读,别为了“链”而链
自定义异常必须支持双参构造函数
如果你写了 MyOrderException extends RuntimeException,却只实现了 MyOrderException(String message),那别人就无法用它构建异常链——哪怕调用方想传 e,编译都过不去。
- ✅ 必须提供:
public MyOrderException(String message, Throwable cause) { super(message, cause); } - ❌ 不要省略:不写这个构造器,等于主动关闭了异常上下文传递能力
- ? 小技巧:IDE(如 IntelliJ)可一键生成带 cause 的构造器,别手敲漏掉
super(...)
什么时候非用 initCause()?基本不用
initCause() 是给老代码兜底用的:比如你依赖的某个 SDK 里的异常类,是 JDK 1.3 时代写的,压根没提供双参构造器。现代 Java(1.4+)所有标准异常和你自己写的异常,都应该走构造函数路线。
- ⚠️ 高危陷阱:
initCause()必须在throw前调用,且不能在fillInStackTrace()之后调(否则抛IllegalStateException) - ❌ 常见翻车:先
new MyEx("xxx"),再ex.initCause(e),最后忘了throw ex,结果抛出的还是旧异常 - ✅ 现代写法:直接
throw new BusinessException("xxx", e)——安全、简洁、无时机风险
怎么看异常链是否生效?盯紧日志里的 Caused by:
线上出问题时,第一眼就扫日志末尾有没有连续的 Caused by:。有,说明链建对了;没有,说明 somewhere 漏传了 cause。
立即学习“Java免费学习笔记(深入)”;
- ✅ 典型有效日志片段:
com.example.BusinessException: 支付创建失败
at com.example.PaymentService.create(PaymentService.java:42)
Caused by: org.springframework.dao.DataAccessResourceFailureException: Could not obtain JDBC Connection
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:82)
Caused by: java.sql.SQLTimeoutException: Timeout after 30000ms - ❌ 无效日志:只有最上层异常堆栈,下面干干净净,连
Caused by四个字母都没有 - ? 排查建议:从 Controller 层一路往下查 catch 块,凡是有
throw new XxxException(...)的地方,立刻确认参数里有没有e
catch 到异常时,下意识多问一句:“这个 e,我让它继续说话了吗?”漏一次,就可能让一个数据库连接超时,变成三天后还在查“为什么下单没反应”。








