B06 错误处理与调试

2026-10-31 17:140 阅读约 12 分钟JavaScript

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 验证与讨论

  1. 执行脚本,确认三次 loadConfig 输出与注释一致。
  2. 把 catch 内的 throw err 删除,观察未知错误被静默吞掉,理解为何不应无条件兜底。
  3. 在 createConfig 内设置断点,用「调用栈」面板逐层查看 input 的实际内容。
  4. 把 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

参考文献

[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

0/2000

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