深入理解程序运行时异常:哪些能 try-catch,哪些绝对不能
在实际开发中,很多工程事故并不是因为“异常发生了”,而是因为异常被错误地处理了。 尤其常见的误区是:
“只要 try-catch 住,程序就安全了。”
这是错误的。
本文将系统讲清楚:
- 程序运行中到底有哪些“异常”
- 为什么有些异常可以 try-catch,有些不可以
- 各主流语言(Java / JS / iOS)的设计哲
- 工程实践中真正正确的异常处理方式
一、先给结论(TL;DR)
不是所有运行期错误都应该、也都能够被 try-catch。
从工程角度看,运行期问题可以分为两大类:
| 分类 | 是否应 try-catch | 本质 |
|---|---|---|
| 可恢复异常 | ✅ 可以 | 程序仍处于可控状态 |
| 不可恢复错误 | ❌ 不可以 / 不应该 | 运行环境已损坏 |
区分标准只有一个:
👉 异常发生后,程序是否仍然处于“安全、可预测”的状态?
二、第一类:可以 try-catch 的异常(可恢复异常)
1. 什么是“可恢复异常”
可恢复异常通常具备以下特征:
- 程序控制流仍然存在
- 内存、栈、线程结构完好
- 异常是“业务或环境层面”的问题
- 通过逻辑处理可以修复、降级或提示用户
try-catch 的本质并不是“修复错误”,而是控制流跳转。
2. 常见示例
Java / Kotlin
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("除零异常");
}
常见可捕获异常:
- IllegalArgumentException
- NullPointerException
- IOException
- SQLException
- 业务自定义异常(如 BizException)
这些异常的共同点是:
JVM 仍然可以安全地继续执行后续代码
JavaScript / TypeScript
try {
JSON.parse("invalid json");
} catch (e) {
console.error("JSON 解析失败");
}
可捕获的包括:
- 运行期逻辑错误
- throw new Error()
- Promise.reject() / async-await 异常
但注意:
- 语法错误(parse 阶段)无法 try-catch
Objective-C / Swift
@try {
NSArray *arr = @[];
NSLog(@"%@", arr[1]);
}
@catch (NSException *exception) {
NSLog(@"数组越界");
}
⚠️ 但在 iOS 中:
Apple 并不鼓励使用 try-catch 来处理 NSException
原因后文会详细说明。
3. 适合 try-catch 的典型场景
- 用户输入不合法
- 网络请求失败
- 文件不存在 / 权限不足
- 数据解析失败(JSON / XML)
- 明确可预期的业务异常
一句话总结:
这是一个“可能发生、而且你已经预期到”的问题
三、第二类:不可以 try-catch 的异常(致命错误)
1. 本质区别(非常关键)
这些问题严格来说已经不是“异常”,而是:
程序运行环境本身已经处于不可预测状态
典型特征:
- 内存被破坏
- 栈空间耗尽
- CPU 执行非法指令
- 操作系统强制终止进程
在这种情况下:
继续运行程序,本身就是一种错误
2. 典型示例(语言无关)
① 内存访问错误(C / C++)
int *p = NULL;
*p = 10; // SIGSEGV
- 空指针解引用
- 野指针
- double free ➡️ 结果:
进程被 OS 直接终止,try-catch 根本不存在
② 栈溢出(Stack Overflow)
void recursive() {
recursive();
}
- 栈帧无法再分配
- JVM 无法保证执行环境
- catch 往往来不及执行
③ 系统级崩溃信号
| 信号 | 含义 |
|---|---|
| SIGSEGV | 段错误 |
| SIGABRT | 进程被强制终止 |
| Illegal Instruction | CPU 指令非法 |
| Watchdog Kill | 系统杀死进程 |
这些错误由 操作系统 直接处理,而不是语言运行时。
3. JVM 中的 Error(非常容易被误用)
try {
// do something
} catch (Throwable t) {
// 极其危险
}
在 Java 中:
| 类型 | 是否应捕获 | 说明 |
|---|---|---|
| Exception | ✅ | 业务或环境问题 |
| Error | ❌ | JVM 无法保证安全 |
典型 Error:
- OutOfMemoryError
- StackOverflowError
- InternalError
官方建议:永远不要 catch Error
4. iOS / Objective-C 的特殊说明(重点)
在 iOS 中,大量崩溃来自:
- EXC_BAD_ACCESS
- 野指针
- 访问已释放对象
NSArray *arr = @[];
arr[1]; // 直接崩溃
Apple 官方明确说明:
NSException 不应用于控制业务流程
原因是:
- 抛出异常时,对象状态可能已经被破坏
- catch 后继续执行 = 不确定行为
四、为什么 try-catch 捕获不了这些错误?
一句话答案
try-catch 只能处理“语言级控制流异常”, 而处理不了“运行环境级崩溃”。
对比理解
| 维度 | Exception | Crash / Error |
|---|---|---|
| 控制者 | 语言运行时 | OS / CPU |
| 是否安全 | 是 | 否 |
| 是否应恢复 | 可以 | 不应该 |
五、一个极其常见的工程误区
catch (Exception e) {
// 什么都不做
}
这是反模式:
- 隐藏真实 bug
- 程序处于半失败状态
- 资源未释放
- 后续逻辑更危险
“吞异常”比“崩溃”更可怕
六、正确的工程实践原则
1️⃣ try-catch 的边界原则
只在你“能处理”的地方 catch
能补救 → catch
不能补救 → 抛出 / 终止
2️⃣ 永远不要做的事情
- catch Throwable
- catch Error
- 用异常做业务分支
- 捕获后忽略
3️⃣ 面对不可恢复错误的正确姿势
| 手段 | 目的 |
|---|---|
| Crash 日志 | 定位问题 |
| 崩溃上报 | 评估影响 |
| 防御式编程 | 降低发生概率 |
| 单元测试 | 提前发现 |
七、终极总结
可以 try-catch 的,是“程序还能继续跑的异常”;
不可以 try-catch 的,是“程序已经不该继续跑的错误”。
理解这一点,
- 你会写出更健壮的代码
- 你会少写很多“自以为安全”的 bug
- 你会真正理解语言设计者的取舍