错误监控深度解析:从异常捕获到SourceMap还原
前端错误监控捕获 JS 运行时异常、Promise 拒绝、资源加载失败与 HTTP 错误四类错误,结合 SourceMap 实现源码级定位。 加上用户行为栈与环境信息构建发生到定位到复现到修复的闭环,面试常考 window.onerror 与 unhandledrejection 的捕获要点。
一句话概括
前端错误监控通过捕获 JS 运行时异常、Promise 拒绝、资源加载失败及 HTTP 请求错误四类错误,结合 SourceMap 实现源码级定位,加上用户行为栈和环境信息,构建”发生→定位→复现→修复”的完整闭环。
核心知识点
1. 前端错误的四大类型与捕获方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ① JS 运行时异常 → window.onerror / addEventListener('error')
window.addEventListener('error', (event) => {
if (event instanceof ErrorEvent) {
console.log('JS Error:', event.message, event.filename, event.lineno);
}
});
// ② Promise 拒绝 → unhandledrejection
window.addEventListener('unhandledrejection', (event) => {
console.log('Promise Rejection:', event.reason);
event.preventDefault(); // 阻止控制台输出
});
// ③ 资源加载错误 → addEventListener('error', handler, true)【捕获阶段!】
window.addEventListener('error', (event) => {
const target = event.target;
if (target instanceof HTMLImageElement) {
console.log('Image 加载失败:', target.src);
}
}, true); // ⚠️ 资源错误不冒泡,必须在捕获阶段监听
// ④ HTTP 请求错误 → 劫持 XMLHttpRequest / fetch
关键陷阱:资源加载错误不冒泡!window.addEventListener('error', handler) 默认在冒泡阶段收不到。必须加 true 第三个参数在捕获阶段监听。
2. SourceMap 还原:从压缩代码到源码定位
1
2
3
4
5
6
7
8
9
10
11
12
const { SourceMapConsumer } = require('source-map');
// 加载 .js.map 文件
const consumer = await new SourceMapConsumer(mapContent);
// 将压缩位置还原为源码位置
const original = consumer.originalPositionFor({
line: 1, // 压缩代码的行号
column: 12345 // 压缩代码的列号
});
// → { source: 'app.tsx', line: 42, column: 8, name: 'getUserName' }
关键最佳实践:SourceMap 绝不部署到 CDN!.map 文件只存储在内网监控服务中,收到错误上报后由服务端还原。否则任何人都能通过 DevTools 看到你的源码。
3. 错误上报的可靠性保障
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 三层保障策略(优先级从高到低)
function guaranteedReport(data) {
const body = JSON.stringify(data);
// ① sendBeacon:页面卸载时也能发送,不受取消影响
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/error', body);
return;
}
// ② fetch keepalive:页面关闭后继续发送
fetch('/api/error', {
method: 'POST', body,
keepalive: true,
headers: { 'Content-Type': 'application/json' }
}).catch(() => {});
// ③ Image ping 兜底:小数据量时使用(GET 请求,长度有限制)
new Image().src = `/api/error?data=${encodeURIComponent(body)}`;
}
4. “Script error.” 问题的根因与修复
1
2
3
4
5
6
7
8
9
<!-- 问题:CDN 脚本出错后,浏览器只返回 "Script error." 无详情 -->
<!-- 修复两步: -->
<!-- 1. CDN 返回 CORS 头:Access-Control-Allow-Origin: * -->
<!-- 2. script 标签加 crossorigin -->
<script src="https://cdn.example.com/lib.js" crossorigin="anonymous"></script>
<!-- 原理:浏览器的同源策略阻止读取跨域脚本的错误详情 -->
<!-- crossorigin="anonymous" 告诉浏览器"以 CORS 方式请求此脚本" -->
5. 错误聚合与告警去重
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误指纹:用错误栈前 3 帧生成唯一标识
function generateFingerprint(errorEvent) {
const frames = errorEvent.stack
.split('\n')
.filter(l => l.includes('at '))
.slice(0, 3)
.map(l => l.replace(/\d+/g, '<N>')) // 替换行号为占位符
.join('|');
return hash(`${errorEvent.name}:${errorEvent.message}:${frames}`);
}
// 聚合逻辑:同指纹 5 分钟内只告警一次
// 防止一个 bug 导致每分钟数万条告警
其实你每天都在用
- Sentry / 自研监控后台的 SourceMap 还原:生产环境看到
at e(anonymous_3c@app.js:1:12345)完全看不懂,监控后台自动还原成at getUserName (app.tsx:42:8)才是可读的错误栈 <img src="broken.jpg" onerror="...">:图片加载失败时的兜底处理,就是一种最基础的资源错误捕获- React Error Boundary:
componentDidCatch捕获子组件渲染错误,防止整个应用白屏——配合监控 SDK 把错误信息上报 - axios 拦截器的全局错误处理:在
response.interceptors中统一处理 401/403/500,避免每个 API 调用都要写 try-catch console.log('xxx')作为”最原始的监控”:当然这也暴露了为什么需要专业的错误监控——console.log在生产环境不可见
常见误解(FAQ)
❌ 误区 1:「try-catch 能捕获所有错误」
try-catch 只能捕获同步代码中的异常。setTimeout(() => { throw ... })、Promise.reject()、事件回调中的错误都在 try-catch 的作用域链之外。这些必须通过全局的 onerror 和 unhandledrejection 来兜底。
❌ 误区 2:「生产环境把 SourceMap 一起部署就行,反正大部分用户不会看」
任何用户打开 DevTools → Sources 面板就能下载你的源码。关键是源码泄露比”部分用户能看”还严重:竞争对手直接看你的业务逻辑、API 调用方式、内部架构。
❌ 误区 3:「window.onerror 和 addEventListener('error') 是一样的」
两者有本质区别:onerror 只能有一个 handler(后续覆盖前面的),参数是原始字符串;addEventListener 可以多个 handler,参数是 ErrorEvent 对象。且资源加载错误只能通过 addEventListener('error', ..., true) 捕获——onerror 完全拿不到资源错误。
❌ 误区 4:「有了错误监控就没必要做 Code Review 和测试了」
错误监控是事后发现,Code Review 和测试是事前预防。好的错误监控可以告诉你”哪里出问题了”,但不会阻止 bug 的上线。三者是互补的:测试防大部分 bugs、CR 防逻辑错误、监控兜底线上异常。
一句话总结
错误监控不是”出了问题才想起来看的东西”,而是在用户告诉你之前,你就已经知道出事了的工程体系——最好的监控,是让用户永远不需要提交 Bug Report。