Skip to main content
错误处理我从线上监控倒推:错误怎么捕获、Source Map 怎么还原、框架层怎么兜底,这三段串起来才算闭环。

Q1: 错误处理的历史?为什么要有它?

1

1960s-70s: 无错误处理的年代

早期程序遇到错误直接崩溃。程序员需要手动检查每一个函数的返回值来判断是否出错——这种方式容易遗漏,且代码充斥着 if (error) return error 的样板代码。
2

1990s: try/catch 异常机制引入

Java 和 C++ 引入了结构化异常处理(Structured Exception Handling):try/catch/finally。JavaScript 在 ES3(1999 年)也引入了这套机制。核心思想是:正常逻辑和错误处理逻辑分离,用 throw 抛出错误,用 catch 捕获。
3

2015年: Promise 链式错误处理

ES6 引入 Promise,提供了 .catch() 方法处理异步错误。但 Promise 链的错误处理容易遗漏——忘记写 .catch() 会产生未处理的 rejection(浏览器触发 unhandledrejection,Node.js 触发 unhandledRejection)。
4

2017年: async/await 统一错误处理

ES2017 引入 async/await,让异步代码可以用 try/catch 捕获错误,终于统一了同步和异步的错误处理方式。
错误处理解决的核心问题:
错误处理的本质是:将不可预期的异常从”程序崩溃”变成”可控的分支逻辑”。

🚫 没有错误处理

像开车没有刹车——一切顺利时飞驰,但遇到突发情况(红灯、行人)就只能撞上去。代码中一个未捕获的错误就能让整个应用白屏。

✅ 有错误处理

像开车有刹车和安全气囊——正常行驶时没有区别,但遇到突发情况可以安全停车。try/catch 是刹车,finally 是安全气囊(保证资源一定被释放),全局错误监控是行车记录仪(记录事故现场)。
回答错误处理时,先说”为什么要处理”(用户体验、系统稳定性),再说”怎么处理”(try/catch、Promise.catch、全局监控)。想听的是你对”防御性编程”的理解,而不是背诵 API。

Q2: Error 对象详解

JavaScript 的所有错误都是 Error 对象或其子类的实例。理解 Error 对象是正确处理错误的基础。

JavaScript 内置的错误类型

Error.captureStackTrace 是 V8 提供的非标准 API,可以精确控制 stack 属性中显示的调用栈:

自定义错误类型

Error 就像医院的总挂号处。TypeError 是内科、ReferenceError 是外科、SyntaxError 是耳鼻喉科——每个科室专门处理一类问题。自定义错误(如 HttpError)就像医院新开的专科门诊,专门处理特定业务的问题。有了分诊系统(instanceof 判断),医生(catch 块)就能快速找到正确的处理方案。

Error.cause(ES2022)

ES2022 引入了 Error.cause,用于在重新抛出错误时保留原始错误信息:

Q3: try/catch/finally 的执行机制

try/catch/finally 的执行顺序看似简单,但有一些容易忽略的细节。
finally 块无论是否发生错误都会执行,甚至在 try 或 catch 中有 return 语句时也会执行:
更极端的情况:

块级作用域与错误变量

try/catch 只能捕获同步错误


Q4: Promise 错误处理

Promise 提供了 .catch() 方法来处理异步错误,但错误传播规则和同步代码不同。
如果 Promise 链中没有 .catch(),rejection 就会变成”未处理”:浏览器在每个微任务检查点后触发 unhandledrejection 事件,Node.js 触发 unhandledRejection 事件。在 Node.js 15+ 中,未处理的 Promise rejection 默认会直接终止进程。

全局捕获未处理的 Promise 错误

Promise.allSettled vs Promise.all

Promise.all 就像考试——一科不及格就留级(整体失败),其他科的成绩作废。Promise.allSettled 就像体检——每项检查结果独立记录,某项异常不会影响其他项的结果。在实际项目中,allSettled 往往更实用——你通常需要知道每项操作的结果,而不是因为一项失败就放弃所有。

Q5: 异步错误处理(async/await)

async/await 让异步代码的错误处理回归到同步的 try/catch 模式,大大简化了代码。

优雅的错误处理模式

模式一:错误优先返回值(Go 风格)
模式二:统一错误处理函数
模式三:try/catch/finally 清理资源

Q6: 前端错误监控与上报

生产环境中的错误处理不只是 try/catch——你需要一个完整的错误监控体系来捕获、收集和分析线上错误。

捕获全局错误

window.onerror 只能捕获同步错误,不能捕获:
  • Promise rejection(需要用 unhandledrejection)
  • 资源加载错误(需要用 error 事件的捕获阶段)
  • 跨域脚本的错误(需要设置 CORS 和 crossorigin 属性)

使用 Beacon API 无损上报

Source Map 还原线上错误

线上代码经过压缩和混淆,错误堆栈无法直接定位到源码。Source Map 可以将压缩后的行号映射回源码位置:
一个完整的前端错误监控体系应包含:
  1. 错误捕获:window.onerror + unhandledrejection + 资源错误监听
  2. 上下文收集:错误堆栈、用户行为路径(面包屑)、设备信息、网络状态
  3. 可靠上报:navigator.sendBeacon 或 keepalive: true 的 fetch
  4. Source Map 还原:在服务端将压缩堆栈还原为源码位置
  5. 告警与分析:按错误频率、影响用户数排序,优先修复高频错误

React 中的错误边界

React 错误边界不能捕获以下错误:
  • 事件处理函数中的错误(用普通的 try/catch)
  • 异步代码(setTimeout、Promise)
  • 服务端渲染(SSR)的错误
  • 错误边界组件自身的错误

Q7: 经典面试题:如何设计一个前端错误监控系统?

设计思路:

底层源码:V8 是如何实现 try/catch 的?

V8 编译 JavaScript 时,会为 try/catch 生成异常处理器表(Exception Handler Table),详见 V8 源码:src/interpreter/bytecode-array-builder.cc:
这段代码说明:try/catch 不是通过”检查返回值”实现的,而是通过异常处理器表——当 try 块中任何位置抛出错误时,V8 直接跳转到 catch 块的入口,不需要逐行检查。 V8 在 Promise 被 reject 时会先检查是否有 handler:若没有,就在微任务检查点(microtask checkpoint)通过 promise rejection 回调通知宿主环境(Blink 或 Node.js)。宿主再决定抛出什么——Blink 派发 unhandledrejection 事件,Node.js 触发 unhandledRejection 事件。也就是说,事件本身由宿主触发,V8 负责跟踪”未处理”状态。详见 V8 源码:src/objects/promise.cc。

及格回答 vs 满分回答

问题:“说说前端的错误处理策略”

及格回答(60-70分):
“用 try/catch 捕获同步错误,用 .catch() 处理 Promise 错误。用 window.onerror 监听全局错误并上报。”
问题出在:知道 API 但缺乏系统性设计。 满分回答(90-100分):
前端错误处理分为三个层次:局部捕获、全局兜底、监控上报。局部捕获用 try/catch 处理已知可能出错的同步代码,用 async/await + try/catch 处理异步逻辑,用 React 错误边界捕获组件渲染错误。全局兜底用 window.onerror 捕获未预期的同步错误,用 unhandledrejection 捕获未处理的 Promise rejection。[1]
错误上报的关键是可靠性和上下文。可靠性通过 navigator.sendBeacon 保证页面关闭时不丢失错误数据。上下文包括错误堆栈、用户行为路径(breadcrumb)、设备信息和页面 URL。线上错误堆栈需要通过 Source Map 还原到源码位置,否则压缩后的行号无法定位问题。[2]
自定义错误类型是大型项目的关键实践。通过继承 Error 创建 HttpError、ValidationError 等业务错误类型,在 catch 块中用 instanceof 做分类型处理,比判断错误消息字符串更可靠。ES2022 的 Error.cause 可以在重新抛出错误时保留原始错误链,避免信息丢失。
实际项目建议:错误监控体系要和 CI/CD 结合——线上错误告警要在几分钟内触达开发团队,高频错误要自动关联到最近的代码变更。错误修复的优先级按”影响用户数 x 错误频率”排序,而不是按错误类型。

常见追问

答题要点: 明确区分同步和异步场景,给出具体示例。高分回答: “try/catch 只能捕获同步错误。对于异步代码,需要分场景讨论:
  1. setTimeout/setInterval 回调:外层 try/catch 无法捕获
  1. await 表达式:外层 try/catch 可以捕获
  1. Promise.then 回调:需要 .catch() 或 await
核心区别:await 会把 Promise rejection 转化为同步的 throw,所以外层 try/catch 可以捕获。”
答题要点: 从 V8 引擎的角度解释调用栈捕获。高分回答: “当 new Error() 被调用时,V8 引擎会同步地捕获当前的调用栈。具体实现是 V8 遍历当前的所有栈帧(Stack Frame),将每个函数名、文件路径、行号、列号拼接成一个字符串,赋值给 error.stack 属性。
注意两个细节:
  1. stack 是非标准属性,但所有主流引擎(V8、SpiderMonkey、JavaScriptCore)都支持。
  2. 捕获调用栈有性能开销——如果在热路径中频繁 new Error(),会影响性能。V8 使用 Error.captureStackTrace(obj, constructorOpt) 可以排除构造函数本身的栈帧。”
答题要点: 说明 finally 的 return 会覆盖 try/catch 中的 return 值。高分回答: “finally 块中的 return 会覆盖 try 或 catch 块中的 return 值。这是一个容易忽略的陷阱:
这是因为 finally 块的执行优先级最高。即使 try 或 catch 中有 return,引擎也会先执行 finally,然后才真正返回。如果 finally 中也有 return,它就成为最终的返回值。最佳实践:永远不要在 finally 中写 return。 finally 应该只做清理工作(关闭连接、释放资源),不做业务逻辑。”
答题要点: 区分浏览器和 Node.js 环境,说明事件名称和处理方式。高分回答: “浏览器和 Node.js 使用不同的事件来捕获未处理的 Promise rejection:浏览器端:
Node.js 端:
区别:
  • 浏览器端:event.reason 是 rejection 的值,event.preventDefault() 可以阻止控制台报错
  • Node.js 端:Node.js 15+ 将未处理的 rejection 视为致命错误,默认终止进程
  • 两个环境都可以用 rejectionhandled 事件监听”后来被处理的 rejection”
最佳实践:在应用入口处注册全局监听器,所有未预期的 rejection 都应该上报到监控系统。”
答题要点: 明确说明错误边界的捕获范围和限制。高分回答: “不能。 React 错误边界(Error Boundary)只能捕获以下错误:
  1. 子组件渲染过程中的错误
  2. 生命周期方法中的错误
  3. 错误边界组件树中构造函数的错误
以下错误不能被错误边界捕获:
  1. 事件处理函数中的错误(用普通的 try/catch)
  2. 异步代码(setTimeout、Promise)
  3. 服务端渲染(SSR)的错误
  4. 错误边界自身的错误
为什么这样设计?因为事件处理函数是用户行为触发的,错误应该是可预期的,应该由开发者主动处理。而渲染错误往往是不可预期的(如数据格式异常),需要错误边界做兜底。”
答题要点: 对比传统做法和 Error.cause 的优劣。高分回答: “Error.cause 是 ES2022 引入的特性,用于在重新抛出错误时保留原始错误链。传统做法是手动附加原始错误:
使用场景:
  1. 分层架构中,底层错误需要包装为业务错误重新抛出
  2. 第三方库错误需要添加更友好的上下文信息
  3. 错误监控系统需要完整的错误链来定位根因
注意事项:Error.cause 在旧浏览器中不支持,需要 polyfill 或使用 TypeScript 的 error.cause 类型检查。”
答题要点: 从调用栈的角度解释两者的根本区别。高分回答: “同步 throw 和异步 Promise.reject 在错误捕获机制上有根本区别:同步 throw:
异步 reject:
核心区别:
  1. 调用栈完整性:同步 throw 保留完整的调用链;异步 reject 的栈可能丢失原始调用者
  2. 执行流:同步 throw 立即中断;reject 不会中断,需要等到 .catch() 或 await 才被捕获
  3. 错误传播:同步 throw 沿调用栈向上传播;reject 沿 Promise 链传播
这就是为什么前端错误监控中,异步错误的堆栈信息往往不如同步错误完整。解决方案是使用 Error.cause 在每层异步调用中保留原始错误。“

延伸阅读与引用参考