文章

错误处理体系与全局捕获

内置 Error 类型怎么分、try/catch 与 Promise 的捕获边界、window.onerror / error 事件 / unhandledrejection 三件套做全局监控,以及如何写可维护的自定义错误。异步错误处理细节另文展开。

错误处理体系与全局捕获

一句话概括

错误处理面试题,考的是三件事:你知不知道有哪些内置错误类型、清不清晰”哪些错误能抓哪些抓不到”、以及全局怎么兜底监控。记住主线:同步用 try/catch,Promise 用 .catch/async-await,漏网的运行时错误靠 window.onerror、资源错误靠 error 事件捕获阶段、未处理 Promise 拒绝靠 unhandledrejection——把这套体系讲明白,基本就过关了。

核心知识点

1. 内置错误类型(面试官常让你列举)

除了通用的 Error,JS 还有一批继承自 Error 的标准错误构造函数。面试能把下面这几个名字说全,再各配一句”什么时候抛”,就很加分:

错误类型触发场景
Error通用错误,也是自定义错误的基类
TypeError变量/参数不是预期类型(null.x、undefined())
ReferenceError引用了不存在的变量(console.log(a) 而 a 未声明)
RangeError数值超出有效范围(递归过深、toFixed 传负数)
SyntaxError语法错误(通常解析阶段就报,运行时少见)
URIErrorencodeURI/decodeURI 收到非法参数
EvalError早期 eval 相关错误,现代基本不抛
AggregateErrorES2021 新增,多个错误聚合(如 Promise.any 全失败时)
1
2
3
4
5
6
7
8
// 用 constructor / instanceof 做精准分流
try {
  null.foo();
} catch (e) {
  if (e instanceof TypeError) { /* 类型错 */ }
  else if (e instanceof RangeError) { /* 范围错 */ }
  else { throw e; } // 不认识的往上抛,别自己吞掉
}

AggregateError 是 ES2021 跟着 Promise.any 一起来的,记住它的触发点(多个 Promise 全部 reject 时聚合错误信息),是个加分项。

2. 捕获的边界:try/catch 抓不到异步回调里的错

这是最高频的坑,try/catch 只作用于当前的同步执行上下文。一旦错误发生在异步回调(宏任务/微任务)里,那个回调执行时 try/catch 早就退出了作用域,自然抓不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 外层 try 抓不到 setTimeout 里的错误
try {
  setTimeout(() => { throw new Error('boom'); }, 0);
} catch (e) {
  console.log('这里永远不会执行');
}

// ✅ 要么在异步回调内部 try/catch
setTimeout(() => {
  try { throw new Error('boom'); } catch (e) { report(e); }
}, 0);

// ✅ 要么靠全局捕获(见第 3 节)

Promise 同理——try { Promise.reject() } 外层抓不到,因为 reject 发生在微任务里;得用 .catch 或 async/await + try/catch(await 会把微任务”拉平”成同步风格,所以 await 后的错误能抓到)。

3. 全局捕获三件套

对于”漏网”的、没被业务代码 catch 的错误,要靠全局机制兜底(典型用途是错误上报监控)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ① 同步运行时错误:window.onerror(拿到最完整的 stack)
window.onerror = (message, source, lineno, colno, error) => {
  report({ message, source, lineno, colno, stack: error?.stack });
  return true; // 注意:onerror 返回 true 才阻止错误打印到控制台(和其他事件相反)
};

// ② 资源加载错误(img/script/link 加载失败)+ 运行时错误
//    资源错误不冒泡,必须在"捕获阶段"(第三个参数 true)才能拿到
window.addEventListener('error', (e) => {
  const t = e.target;
  if (t instanceof HTMLImageElement || t instanceof HTMLScriptElement) {
    report({ type: 'resource', url: t.src || t.href }); // 资源加载失败
  }
}, true);

// ③ 未处理的 Promise 拒绝(reason 即拒绝原因)
window.addEventListener('unhandledrejection', (e) => {
  report({ type: 'promise', reason: e.reason });
});

三个分工要讲清:onerror 只管同步运行时错误;资源加载失败得用 error 事件在捕获阶段捞;未 catch 的 Promise 拒绝 onerror 和 error 都管不着,专属 unhandledrejection。另外跨域脚本报错默认只给 "Script error.",需要 <script crossorigin> + 服务端 Access-Control-Allow-Origin 才能拿到详情。

4. 自定义错误:class extends Error 的正确写法

业务里经常需要”带错误码、能 instanceof 区分”的错误。标准写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ValidationError extends Error {
  constructor(message, code) {
    super(message);
    this.name = 'ValidationError'; // 现代引擎其实会自动设为类名,显式写更稳
    this.code = code;              // 自定义业务码,方便上报/分发
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, ValidationError); // 让 stack 从抛出处开始,隐藏本构造函数
    }
  }
}

// 使用:既能 instanceof 判断,又能带上结构化信息
try {
  throw new ValidationError('参数不合法', 'E400');
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.code); // 'E400',精准分发
  }
}

坑点:Error.captureStackTrace 是 V8(Chrome/Node)非标准 API,用来让 stack 排除构造函数本身;在需要兼容非 V8 环境时它不存在,要做存在性判断。现代原生 class extends Error 一般没问题,但老转译器(Babel < 7)会让子类丢原型链和 name,需要手动 Object.setPrototypeOf 兜底。

5. ES2022 的 cause:错误链别再丢了

以前我们习惯 throw new Error('上层失败: ' + e.message) 把原始错误塞进字符串,原始 stack 就断了。ES2022 提供了 cause 选项,把原始错误作为”原因”挂上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
function doWork() {
  try {
    riskyCall();
  } catch (err) {
    // ✅ 把原始错误作为 cause 传进去,错误链完整保留
    throw new Error('业务层处理失败', { cause: err });
  }
}

try { doWork(); }
catch (e) {
  console.log(e.cause); // 原始底层错误,stack 也在
}

自定义错误也支持 cause:子类构造函数调用 super(message, { cause }) 透传即可。这比自己拼字符串专业得多,是近几年面试喜欢追的点。

其实你每天都在用

  • 表单校验:参数不对就 throw new ValidationError(msg, code),接口层按 code 分发提示文案。
  • 接口请求:fetch 失败 / 状态码非 2xx,在封装层 throw 出带业务信息的错误,页面统一 catch 弹 Toast。
  • 前端监控 SDK:正是用第 3 节的 onerror + unhandledrejection 把未捕获错误上报到埋点平台。
  • async/await 函数:try { await api() } catch(e) { ... } 是日常最普适的捕获写法。
  • Promise.any:多个源并发取最快成功,全失败就抛出 AggregateError,需要 e.errors 看全部失败原因。

常见误解(FAQ)

❌ 误区1:”try/catch 能抓到所有错误,包括异步里的。” 错。try/catch 只管当前同步上下文,setTimeout / Promise 回调里的错误它抓不到,要么内部 catch,要么交给全局捕获。

❌ 误区2:”资源加载失败用 window.onerror 就能抓。” 错。onerror 只捕获同步运行时错误;<img>/<script> 加载失败触发的 error 事件不冒泡,必须在 addEventListener('error', handler, true) 的捕获阶段才能拿到,并靠 event.target 区分。

❌ 误区3:”unhandledrejection 和 onerror 是一回事。” 错。未 catch 的 Promise 拒绝不会冒泡到 onerror,它有专属事件 unhandledrejection,错误信息在 event.reason 上。

❌ 误区4:”catch 里什么都不做(空 catch)最省事。” 错。空 catch 会”吞掉”错误,线上排障时你根本不知道哪里炸了。至少 console.error 或上报;能恢复就处理,不能恢复就 throw e 继续往上抛。

❌ 误区5:”window.onerror 返回 false 能阻止控制台报错。” 错。恰恰相反——onerror 返回 true 才阻止默认打印(和其他事件 return false 取消默认行为相反),这是历史包袱,容易记反。

一句话总结

内置错误按类型分、同步用 try/catch、Promise 用 .catch、漏网的靠 onerror/error(捕获阶段)/unhandledrejection 三件套兜底,自定义错误 extends Error 带上业务码与 cause 错误链——能抓的精准抓,抓不到的兜底上报,别让任何错误悄悄溜走。

本文由作者按照 CC BY 4.0 进行授权