Java异常默认向上抛出:运行时异常自动传播,受检异常须声明或捕获;应封装异常链而非吞掉或重复包装;finally中抛异常会覆盖主异常;异步异常需显式获取,资源关闭推荐try-with-resources。

Java中异常不捕获时会自动向上抛出
Java的异常传播机制决定了:只要方法里没用 try-catch 捕获,且该异常是 RuntimeException 及其子类(运行时异常),或声明了 throws 的受检异常,它就会沿调用栈逐层向上传递,直到被处理或终止线程。
这意味着你不需要在每一层都写 throw e —— 它默认就“冒泡”上去。但关键点在于:受检异常(如 IOException、SQLException)必须显式声明或捕获,否则编译失败;而 NullPointerException 这类运行时异常完全跳过编译检查。
- 如果 A 调用 B,B 调用 C,C 抛出
IllegalArgumentException(运行时异常),A 不写任何try-catch也能编译通过,异常最终由 JVM 处理并打印堆栈 - 如果 C 抛出
FileNotFoundException(受检异常),那么 B 必须要么try-catch,要么在方法签名加throws FileNotFoundException;同理,A 也得对这个异常做同样选择 - 别试图用空
catch吞掉异常再“静默返回”,这会让上层完全无法感知失败,调试时只剩NullPointerException这种二次异常
多层调用中如何包装和重抛异常
原始异常信息常含敏感路径或底层实现细节(比如数据库连接串、文件绝对路径),直接抛给上层不安全也不友好。更常见的做法是用 throw new BusinessException("订单创建失败", e) 这类方式封装。
这种“异常链”保留了原始根因(可通过 e.getCause() 获取),又提供了业务语义。但要注意:不要重复包装同一异常,否则堆栈里出现多层相同类型,干扰问题定位。
立即学习“Java免费学习笔记(深入)”;
- 推荐用构造函数带
Throwable cause参数的子类,如IllegalArgumentException(String msg, Throwable cause) - 避免写
throw new RuntimeException(e)—— 这会丢失原始异常类型,让上层无法用instanceof区分处理逻辑 - Spring 等框架常用
@ExceptionHandler统一处理顶层异常,此时异常是否被包装,直接影响响应码和错误消息的生成逻辑
finally块中抛异常会覆盖主流程异常
这是最容易踩坑的地方:当 try 块已抛出异常,而 finally 块里又发生新异常(比如关闭资源时 IO 失败),JVM 会丢弃 try 中的原始异常,只抛出 finally 里的那个。
例如下面代码:
public void readFile() throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream("missing.txt"); // 抛出 FileNotFoundException
return;
} finally {
if (fis != null) fis.close(); // close() 抛出 IOException
}
}
调用方看到的是 IOException,而真正的 FileNotFoundException 被吞掉了。
- JDK 7+ 推荐用
try-with-resources自动管理资源,它会在多个异常同时发生时把次要异常作为 suppressed exception 附加到主异常上(可用e.getSuppressed()查看) - 若必须手写
finally,关闭资源时要用try-catch包裹,并记录日志,而不是再次throw - 尤其注意日志框架的
close()、数据库连接池的close()、HTTP client 的shutdown(),它们都可能抛异常
异步调用(CompletableFuture / Thread)中断异常传播链
在 CompletableFuture.supplyAsync() 或新启线程中抛出的异常,默认不会传回主线程,而是被吃掉或仅打印到 stderr。主线程继续执行,像什么都没发生。
比如:
CompletableFuture.runAsync(() -> {
throw new RuntimeException("后台任务炸了");
}); // 这个异常永远不会到达主线程
- 必须显式调用
.join()或.get()才能触发异常传播;.get()会把原始异常包进ExecutionException,需用e.getCause()解包 - 用
.exceptionally()或.handle()注册回调来捕获异常,适合做降级或告警,但不能替代同步传播 - 线程池中未捕获的异常走
Thread.UncaughtExceptionHandler,别依赖默认行为——它通常只打日志,不中断流程也不通知上游








