错误处理我从线上监控倒推:错误怎么捕获、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 是安全气囊(保证资源一定被释放),全局错误监控是行车记录仪(记录事故现场)。Q2: Error 对象详解
JavaScript 的所有错误都是Error 对象或其子类的实例。理解 Error 对象是正确处理错误的基础。
JavaScript 内置的错误类型
自定义错误类型
Error.cause(ES2022)
ES2022 引入了Error.cause,用于在重新抛出错误时保留原始错误信息:
Q3: try/catch/finally 的执行机制
try/catch/finally 的执行顺序看似简单,但有一些容易忽略的细节。
块级作用域与错误变量
try/catch 只能捕获同步错误
Q4: Promise 错误处理
Promise 提供了.catch() 方法来处理异步错误,但错误传播规则和同步代码不同。
全局捕获未处理的 Promise 错误
Promise.allSettled vs Promise.all
Q5: 异步错误处理(async/await)
async/await 让异步代码的错误处理回归到同步的 try/catch 模式,大大简化了代码。
优雅的错误处理模式
模式一:错误优先返回值(Go 风格)Q6: 前端错误监控与上报
生产环境中的错误处理不只是try/catch——你需要一个完整的错误监控体系来捕获、收集和分析线上错误。
捕获全局错误
使用 Beacon API 无损上报
Source Map 还原线上错误
线上代码经过压缩和混淆,错误堆栈无法直接定位到源码。Source Map 可以将压缩后的行号映射回源码位置:React 中的错误边界
Q7: 经典面试题:如何设计一个前端错误监控系统?
设计思路:底层源码:V8 是如何实现 try/catch 的?
V8 的 try/catch 在字节码层面是怎么实现的?
V8 编译 JavaScript 时,会为 try/catch 生成异常处理器表(Exception Handler Table),详见 V8 源码:src/interpreter/bytecode-array-builder.cc:
try/catch 不是通过”检查返回值”实现的,而是通过异常处理器表——当 try 块中任何位置抛出错误时,V8 直接跳转到 catch 块的入口,不需要逐行检查。
Promise 的 rejection 是怎么被 unhandledrejection 事件捕获的?
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 能捕获异步错误吗?
try/catch 能捕获异步错误吗?
答题要点:
明确区分同步和异步场景,给出具体示例。高分回答:
“核心区别:
try/catch 只能捕获同步错误。对于异步代码,需要分场景讨论:- setTimeout/setInterval 回调:外层 try/catch 无法捕获
- await 表达式:外层 try/catch 可以捕获
- Promise.then 回调:需要 .catch() 或 await
await 会把 Promise rejection 转化为同步的 throw,所以外层 try/catch 可以捕获。”Error 对象的 stack 属性是怎么生成的?
Error 对象的 stack 属性是怎么生成的?
答题要点:
从 V8 引擎的角度解释调用栈捕获。高分回答:
“当 注意两个细节:
new Error() 被调用时,V8 引擎会同步地捕获当前的调用栈。具体实现是 V8 遍历当前的所有栈帧(Stack Frame),将每个函数名、文件路径、行号、列号拼接成一个字符串,赋值给 error.stack 属性。stack是非标准属性,但所有主流引擎(V8、SpiderMonkey、JavaScriptCore)都支持。- 捕获调用栈有性能开销——如果在热路径中频繁
new Error(),会影响性能。V8 使用Error.captureStackTrace(obj, constructorOpt)可以排除构造函数本身的栈帧。”
finally 块中的 return 会怎样?
finally 块中的 return 会怎样?
答题要点:
说明 finally 的 return 会覆盖 try/catch 中的 return 值。高分回答:
“这是因为
finally 块中的 return 会覆盖 try 或 catch 块中的 return 值。这是一个容易忽略的陷阱:finally 块的执行优先级最高。即使 try 或 catch 中有 return,引擎也会先执行 finally,然后才真正返回。如果 finally 中也有 return,它就成为最终的返回值。最佳实践:永远不要在 finally 中写 return。 finally 应该只做清理工作(关闭连接、释放资源),不做业务逻辑。”如何全局捕获未处理的 Promise rejection?
如何全局捕获未处理的 Promise rejection?
答题要点:
区分浏览器和 Node.js 环境,说明事件名称和处理方式。高分回答:
“浏览器和 Node.js 使用不同的事件来捕获未处理的 Promise rejection:浏览器端:Node.js 端:区别:
- 浏览器端:
event.reason是 rejection 的值,event.preventDefault()可以阻止控制台报错 - Node.js 端:Node.js 15+ 将未处理的 rejection 视为致命错误,默认终止进程
- 两个环境都可以用
rejectionhandled事件监听”后来被处理的 rejection”
React 错误边界能捕获事件处理函数中的错误吗?
React 错误边界能捕获事件处理函数中的错误吗?
答题要点:
明确说明错误边界的捕获范围和限制。高分回答:
“不能。 React 错误边界(Error Boundary)只能捕获以下错误:为什么这样设计?因为事件处理函数是用户行为触发的,错误应该是可预期的,应该由开发者主动处理。而渲染错误往往是不可预期的(如数据格式异常),需要错误边界做兜底。”
- 子组件渲染过程中的错误
- 生命周期方法中的错误
- 错误边界组件树中构造函数的错误
- 事件处理函数中的错误(用普通的
try/catch) - 异步代码(
setTimeout、Promise) - 服务端渲染(SSR)的错误
- 错误边界自身的错误
Error.cause 的使用场景和注意事项?
Error.cause 的使用场景和注意事项?
答题要点:
对比传统做法和 Error.cause 的优劣。高分回答:
“使用场景:
Error.cause 是 ES2022 引入的特性,用于在重新抛出错误时保留原始错误链。传统做法是手动附加原始错误:- 分层架构中,底层错误需要包装为业务错误重新抛出
- 第三方库错误需要添加更友好的上下文信息
- 错误监控系统需要完整的错误链来定位根因
Error.cause 在旧浏览器中不支持,需要 polyfill 或使用 TypeScript 的 error.cause 类型检查。”同步 throw 和异步 reject 的错误捕获有什么区别?
同步 throw 和异步 reject 的错误捕获有什么区别?
答题要点:
从调用栈的角度解释两者的根本区别。高分回答:
“同步 异步 reject:核心区别:
throw 和异步 Promise.reject 在错误捕获机制上有根本区别:同步 throw:- 调用栈完整性:同步 throw 保留完整的调用链;异步 reject 的栈可能丢失原始调用者
- 执行流:同步 throw 立即中断;reject 不会中断,需要等到
.catch()或await才被捕获 - 错误传播:同步 throw 沿调用栈向上传播;reject 沿 Promise 链传播
Error.cause 在每层异步调用中保留原始错误。“延伸阅读与引用参考
- 🟢 MDN: Error:Mozilla 官方对 Error 对象及其子类的权威文档。[1]
- 🔗 Sentry Frontend Handbook:Sentry 官方的前端错误监控最佳实践,包含错误捕获、面包屑、Source Map 还原的完整方案。[2]
- 📖 Google Web Fundamentals: Error Handling:Google 对前端错误处理与资源加载监控的深度指南。
