6.1 读懂错误与调用栈
前置知识:第2章终端基础、第5章包管理器
预计时间:15 分钟
阅读方式:先判断失败阶段,再按语言的栈格式取证
完成标志:能区分编译错误与运行时异常,并从 Java、Python 或 Node.js 栈中找出下一条调查线索
程序失败后,Git 能告诉你改了什么,调试器能暂停正在运行的进程。在动手改代码之前,先读懂错误输出和调用栈留下的证据。
程序崩溃了
你加了一行代码,编译通过,启动后屏幕却被错误输出占满。Exception、Thread、NullPointer、at 每个词都认识,连在一起就像错误日志在报复性加班。先别猜:确认它在哪个阶段失败,再读调用路径怎样到达那里。
先分清五件事
| 线索 | 含义 | 不代表什么 |
|---|---|---|
| 编译或解析错误 | 源码尚未通过编译器或解析器 | 不代表程序已经运行 |
| 异常 | 运行时出现了异常条件 | 被捕获的异常不一定终止进程 |
| 调用栈 | 异常时的调用路径快照 | 不自动等于根因 |
stdout / stderr | 两个输出通道 | stderr 有内容不一定等于失败 |
| 退出码 | 进程结束状态 | 具体非零值的含义由程序定义 |
下面的 main.py 是待诊断程序的占位文件名;可以先保存后文的 Python 示例再执行。命令执行后立即查看退出码:
python3 main.py
echo "$?"若要保留证据,记录退出码并把两路输出分别保存;即使调用它的 Shell 开启了 set -e,这里的预期失败也不会跳过记录步骤:
status=0
python3 main.py >stdout.log 2>stderr.log || status=$?
printf 'status=%s\n' "$status"终端颜色不是可靠信号。检查原始文本、退出码和程序文档。
Java:从异常头沿栈帧向调用者走
Exception in thread "main" java.lang.NullPointerException: <诊断消息因 JDK 而异>
at Main.processData(Main.java:8)
at Main.main(Main.java:4)这段信息依次给出:
- 出错线程是
main; - 异常类型是
NullPointerException; - 冒号后是 JDK 提供的诊断消息,具体措辞可能随版本变化;
processData的栈帧更靠近抛出点;main是它的调用者。
把下面内容保存为 Main.java,使用 JDK 的 javac 编译,再用 java 运行。前面的栈是结构示意,文件路径和消息以你的输出为准:
public class Main {
public static void main(String[] args) {
String input = null;
processData(input);
}
static void processData(String data) {
System.out.println(data.length());
}
}javac Main.java
java Main
echo "$?"打开栈帧中的文件与行号,检查这一行用到了哪些值,再追这些值从哪里来。给 data 加空值判断也许能阻止崩溃,却未必解释它为何是空值。
Caused by 和 Suppressed
java.lang.IllegalStateException: cannot load workshop configuration
at com.example.Boot.start(Boot.java:20)
Caused by: java.nio.file.NoSuchFileException: application.properties
at java.base/sun.nio.fs.UnixException.translateToIOException(...)外层异常描述当前抽象层,Caused by: 保存底层原因。继续展开到最内层,但也要回看外层提供的业务上下文。
Suppressed: 通常表示处理主异常时又发生了附加异常,例如关闭资源失败。它不是 Caused by: 的同义词。
Python:类型与消息通常在末尾
def process_data(data):
return len(data)
process_data(None)Traceback (most recent call last):
File "main.py", line 4, in <module>
process_data(None)
File "main.py", line 2, in process_data
return len(data)
TypeError: object of type 'NoneType' has no len()读法:
- 先看末行的
TypeError和消息; - 向上找最后一条与你代码有关的帧;
- 沿更早的帧确认参数是怎样传入的;
- 若出现 “During handling…” 或 “direct cause…”,把前后两个异常一起读。
语法错误则发生在运行前,常带插入符号指出解析器发现问题的位置。那个位置可能只是“终于无法继续解析”的位置,真正漏掉的括号或引号可能在前一行。
Node.js:消息在前,栈帧随后
function processData(data) {
return data.length;
}
processData(null);TypeError: Cannot read properties of null (reading 'length')
at processData (/app/main.js:2:15)
at Object.<anonymous> (/app/main.js:5:1)这段 JavaScript 保存为 main.js 后可用 node main.js 复现;上面的输出展示关键帧,路径随运行目录变化。文件名后通常是 行:列。如果运行的是转译或打包产物,要确认 source map 和部署版本,否则行号可能无法直接对应源码。
不要机械寻找“第一条自己的代码”
第三方库帧很多时,优先找与你的输入、配置和调用有关的边界帧,但不要把库帧全部删掉:
- 库抛出的消息可能准确指出非法参数;
- 框架包装异常后,真正原因可能在后面的
Caused by:; - 异步队列、线程池和远程服务会切断一条本地栈;
- 同名源码若版本不一致,行号会误导。
更稳妥的四问是:
- 最具体的异常类型和消息是什么?
- 哪个栈帧最接近失败操作?
- 哪个上游帧提供了错误输入或状态?
- 是否有异常链、请求 ID 或邻近日志补足上下文?
速查
| 看到什么 | 下一步 |
|---|---|
| 编译器连续报几十个错 | 先修最早一个,再重新编译 |
Java Caused by: | 展开到内层原因,再回看外层语义 |
| Python traceback | 先读末行,再找最后一条相关帧 |
Node file:line:column | 核对实际运行文件和 source map |
| 只有 “process exited” | 保存 stdout、stderr 和退出码 |
| 行号与源码对不上 | 核对构建产物、版本和调试符号 |
离开这一页前
- 运行 Java 或 Python 示例,指出异常类型、消息、失败帧和调用者;
- 解释为什么编译成功不保证运行成功;
- 解释
stderr、异常和非零退出码之间不是一一对应关系; - 遇到异常链时,不丢掉
Caused by:或 Python 的链式异常。
你已经能从红字里分出阶段、异常和调用路径。可半夜没人盯着终端时,现场还得被保存下来。
→ 下一步:日志基础