大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
错误处理与调试
这一课你将能做到
- 读懂控制台报错的「哪一文件、哪一行」,并据此定位问题
- 用
try/catch接住可能失败的代码并给用户提示 - 用断点或
console.log做一次最小排查,形成固定套路
为什么要学
写 JS 一定会报错。
差别在于:有人看红字 10 秒定位;有人盲目改三小时。
调试是技能,不是丢脸。
真实项目里,网络失败、空数据、选择器写错每天都在发生。
会处理错误,你的小工具才配叫「能给别人用」。
先建立图像
出错 → 控制台红色堆栈(stack)
第一行通常最重要:错误类型 + 消息 + 位置
往下看:调用链(谁调用了谁)
常见类型:
| 名字 | 人话例子 |
|---|---|
| SyntaxError | 语法写破(缺括号、缺引号)——往往文件直接无法运行 |
| TypeError | 对 null/undefined 取属性;不是函数却当函数调用 |
| ReferenceError | 变量未定义就用 |
| RangeError | 如错误的数组长度等少见场景 |
排查套路:
1) 读红字第一行
2) 用 log/断点验证「你以为的值」
3) 一次只改一处
4) 需要给用户退路时再包 try/catch
跟着做
步骤 1:故意报错并阅读堆栈
控制台:
const user = null;
user.name; // TypeError: Cannot read properties of null
应看到 TypeError,并指出行号。点开位置,确认「就是这一行」。
再试:
notDefinedNow; // ReferenceError
分清:没声明的名字 vs 有名字但是 null。
步骤 2:try/catch 给退路
function safeName(user) {
try {
return user.name.toUpperCase();
} catch (err) {
// 不要空 catch:至少留下日志
console.error("safeName 失败", err);
return "未知用户";
}
}
console.log(safeName(null)); // 未知用户
console.log(safeName({ name: "ada" })); // ADA
人话:可能炸的地方包起来;失败时给退路,并留下日志。
但不要用 try/catch 掩盖「本该先校验」的逻辑——能提前 if (!user) 更清晰。
步骤 3:页面里的排查套路
保存为 js-errors.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>调试</title>
</head>
<body>
<button type="button" id="btn">跑一下</button>
<p id="out">等待操作</p>
<script>
document.getElementById("btn").addEventListener("click", function () {
const el = document.getElementById("not-exist");
console.log("el 是", el); // 先验证假设:这里多半是 null
const out = document.getElementById("out");
try {
el.textContent = "hi"; // el 为 null 时会 TypeError
} catch (err) {
console.error(err);
out.textContent = "找不到目标元素,请检查 id";
}
});
</script>
</body>
</html>
正确修复方向:先判断 if (!el) { ...; return; },而不是永远靠 catch。
调试步骤:报错前加 console.log,确认假设。
步骤 4:断点(Sources)
- F12 → Sources / 源代码
- 打开你的文件 → 在可疑行号左侧点一下,出现蓝/红断点
- 再点击页面按钮
程序会在此暂停,右侧可看变量当前值。
按 Step / Resume 单步或继续。比狂打 log 更强,适合「值到底怎么变」的问题。
步骤 5:异步错误也要接住
async function load() {
try {
const res = await fetch("/this-will-fail-demo");
if (!res.ok) throw new Error("HTTP " + res.status);
return await res.text();
} catch (err) {
console.error("load 失败", err);
throw err; // 需要时继续往外抛,让 UI 层提示
}
}
load().catch(function (err) {
// 防止未处理的 Promise 拒绝
document.getElementById("out").textContent = "加载失败:" + err.message;
});
async 里用 try/catch 包住 await;若函数继续抛出,调用方还可用 .catch。
步骤 6:最小复现习惯
当你卡住时,新建一个临时 HTML,只保留:
<button type="button" id="btn">test</button>
<script>
// 只留仍能坏的最小代码
document.getElementById("btn").addEventListener("click", function () {
console.log("clicked");
});
</script>
能复现 → 问题边界清楚,好自查也好提问。
不能复现 → 说明问题在你删掉的那部分交互里,继续缩小范围。
讲清楚要点
- 因为堆栈第一行通常指向直接出错处,所以先读红字再改代码。
- 因为空
catch会制造「问题消失」的假象,所以至少console.error,并给用户提示。 - 因为用户输错与程序写错不同,所以输入非法要提示;代码写错要修,别混成一类。
- 因为一次改 20 处无法归因,所以小步修改、每次验证。
- 因为异步错误不会自动变成同步异常,所以
await要配 try/catch 或 Promise.catch。 - 因为假设常常是错的,所以用 log/断点验证「元素是不是 null、值到底是什么」。
你可能会懵的地方
-
异步错误
async里要用 try/catch 包住 await;只写同步 try 包不住后面的 Promise 拒绝。 -
有时堆栈很长
从最上面「你自己的文件」看起,库/框架文件先略读。 -
Source Map
打包后行号可能对应源码映射;开发模式通常较直观。行号对不上时先确认是否看了编译后的文件。 -
要不要到处 try/catch?
不要。边界(网络、解析、用户输入)适合接;内部逻辑错误应尽早暴露,方便修。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 无视红字 | 浪费时间 | 先读第一行与行号 |
| 空 catch | 问题消失假象 | 记录并提示 |
| 一次改 20 处 | 不知哪处生效 | 小步修改 |
| 用 try/catch 代替空值判断 | 控制流变糊 | 能 if (!el) 就先判断 |
| 只在成功路径测 | 上线才炸 | 故意制造失败路径看提示 |
自测三问
- 看到 TypeError 读属性,你首先怀疑什么?
- try/catch 的职责边界是什么?什么情况更适合提前 if?
- 什么叫最小复现?它如何帮助调试?
小结一句话
报错是老师;读堆栈、打日志/断点、小步修复,再用 try/catch 在边界给用户留退路。
延伸
- MDN:错误处理
- MDN:调试 JavaScript
- 下一课:综合练习——串起来做小工具。