B06 错误处理与调试
B06 错误处理与调试
摘要:JavaScript 的错误分编译期语法错误与运行期异常两类,运行期异常沿调用栈向上传播,未被捕获时终止当前任务。
throw抛什么、在哪里捕获、捕获后还抛不抛,决定错误能否被定位。自定义错误类型与统一错误结构让上层能按类别处理。本文给出错误传播路径、三类捕获位置、自定义错误与四步调试流程。
关键词:异常,try catch,错误类型,堆栈,调试
所属:frontend/js / B-进阶 | 上一篇:B05-模块化与ESM | 下一篇:B07-Map与Set集合类型 | 返回:B00-总结
1 引言
页面报错时,控制台第一行给出现象(Cannot read properties of undefined),堆栈首行给出位置(文件名与行号)。多数排查之所以耗时,是因为捕获位置过早:在中间层统一 catch 后只打印一句「请求失败」,丢失了原始堆栈。
本文回答三个问题:错误怎么传播、在哪里捕获、怎么让错误自带上下文。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 类型 | Error、TypeError、RangeError、自定义 |
各浏览器私有错误码 |
| 捕获 | try/catch/finally、Promise 链、全局钩子 |
服务端日志采集 |
| 调试 | 断点、debugger、调用栈、日志分级 |
性能剖析(C03) |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Chrome 130+,开发者工具可用 | Sources 面板能设断点 |
| 前置 | B03 | 理解 Promise 拒绝传播 |
| 文件 | js-practice/errors.js |
编码 UTF-8 |
3 原理与实现
3.1 异常沿调用栈向上传播
throw 之后,当前函数立即中断,控制权交给最近的 catch;一路无捕获则交给全局钩子,如图 3-1 所示。
flowchart TD
A["throw new Error"] --> B{"当前函数有 try"}
B -->|有| C["执行最近 catch"]
B -->|无| D["沿调用栈向上"]
D --> E{"上层有 try"}
E -->|有| C
E -->|无| F["全局处理器<br/>unhandledrejection / onerror"]
F --> G["当前任务终止,控制台报错"]
C --> H["catch 内可处理、可包装后再 throw"]
图 3-1 异常从抛出点向上寻找最近的捕获点
图上可见:
catch之后若不重新抛出,错误就此终止;重新抛出时必须保留原始错误,否则堆栈断链。
3.2 完整实现
// js-practice/errors.js;运行环境:Node.js 20+
// 1 自定义错误:带类别与上下文字段
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// 2 抛出点:只抛具体错误,不抛字符串
function parsePort(raw) {
const n = Number(raw);
if (!Number.isInteger(n) || n < 1 || n > 65535) {
throw new RangeError(`端口超出范围:${raw}`);
}
return n;
}
function createConfig(input) {
if (!input.name) throw new ValidationError('name', '配置名不能为空');
return { name: input.name, port: parsePort(input.port) };
}
// 3 捕获点:分类处理,保留原始错误
function loadConfig(input) {
try {
return { ok: true, data: createConfig(input) };
} catch (err) {
if (err instanceof ValidationError) return { ok: false, code: 'INVALID', field: err.field };
if (err instanceof RangeError) return { ok: false, code: 'RANGE', message: err.message };
throw err; // 未知错误继续上抛,不吞掉
} finally {
// 无论成功失败都执行,适合释放资源
}
}
console.log(loadConfig({ name: 'api', port: '8080' })); // { ok: true, data: { name: 'api', port: 8080 } }
console.log(loadConfig({ port: '8080' })); // { ok: false, code: 'INVALID', field: 'name' }
console.log(loadConfig({ name: 'api', port: '99999' })); // { ok: false, code: 'RANGE', message: '端口超出范围:99999' }
// 4 重新抛出时保留原因
function withContext(fn, label) {
try {
return fn();
} catch (err) {
throw new Error(`${label} 失败`, { cause: err });
}
}
try {
withContext(() => parsePort('x'), '初始化');
} catch (err) {
console.log(err.message, err.cause.name); // 初始化 失败 RangeError
}
// 5 Promise 拒绝也要有归属
const fail = () => Promise.reject(new RangeError('异步失败'));
fail()
.catch((err) => console.log('caught', err.name)) // caught RangeError
.finally(() => console.log('cleanup'));
错误类型与触发场景见表 3-1。
| 类型 | 典型触发 | 处理方式 |
|---|---|---|
SyntaxError |
代码无法解析 | 无法捕获,需修正源码 |
TypeError |
对 undefined 取属性、错误调用 |
入口校验与可选链 |
RangeError |
数值越界、递归过深 | 边界检查 |
ReferenceError |
使用未声明变量 | 修正拼写或声明顺序 |
| 自定义错误 | 业务规则不满足 | 按 instanceof 分类处理 |
调试四步见表 3-2。
| 步骤 | 动作 | 判断依据 |
|---|---|---|
| 1 | 读堆栈第一行定位文件与行号 | 报错位置即抛出点 |
| 2 | 在该行与调用处各设一个断点 | 观察入参是否为预期 |
| 3 | 用「监视」面板固定可疑变量 | 逐帧对比取值变化 |
| 4 | 缩小范围到最小复现 | 能独立复现即定位完成 |
4 验证与讨论
- 执行脚本,确认三次
loadConfig输出与注释一致。 - 把
catch内的throw err删除,观察未知错误被静默吞掉,理解为何不应无条件兜底。 - 在
createConfig内设置断点,用「调用栈」面板逐层查看input的实际内容。 - 把
throw new RangeError(...)换成throw '端口错误',确认instanceof判断失效。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 分类捕获 | 三类结果各自返回对应结构 | instanceof 区分 |
| 未知错误上抛 | 不被静默处理 | 兜底只处理已知类型 |
| 原因保留 | err.cause.name 可读 |
包装时传 cause |
| 字符串抛出 | instanceof Error 为假 |
不要抛非 Error 值 |
讨论与局限。 finally 中 return 会覆盖 try 的返回值,通常只用于释放资源。另,window.onerror 接收不到跨域脚本的详细堆栈,需要服务端返回 Access-Control-Allow-Origin 并给 script 加 crossorigin。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
堆栈只有一行 at ... |
抛的是字符串而非 Error |
统一 throw new Error(...) |
| 报错说「请求失败」但不知原因 | 中间层吞掉了原始错误 | 重新抛出并传 cause |
try/catch 抓不到异步错误 |
未 await 或未返回 Promise |
await 调用,或链尾加 catch |
| 断点不生效 | 加载的是缓存旧文件 | 强刷,或在 Sources 中关闭缓存 |
| 错误只在线上出现 | 压缩后行号错位 | 生成 sourcemap 并上传 |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 异常向上寻找最近捕获点,未捕获即终止任务 | 图 3-1 |
| 2 | 只捕获能处理的类型,未知错误继续上抛 | 3.2 节 |
| 3 | 包装错误必须保留 cause |
表 3-1 |
| 4 | 调试从堆栈第一行开始,逐步缩小范围 | 表 3-2 |
- 上一篇:B05-模块化与ESM
- 下一篇:B07-Map与Set集合类型
- 返回:B00-总结 | 科目 README
参考文献
[1] MDN Web Docs · 控制流与错误处理,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Control_flow_and_error_handling(访问于 2026-10-31)
[2] ECMA-262 · 27.4 Error Objects,https://tc39.es/ecma262/#sec-error-objects(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~