错误采集
🎯 引言
本地开发时我们盯着控制台,报错一目了然;代码到了用户的浏览器里,报了什么错、错在哪一行,我们完全看不到。错误采集就是在用户浏览器里全局监听错误并自动上报给服务器,让线上报错有据可查。
学完本篇,你将能够:
- 用
window.onerror捕获 JS 运行时错误,拿到错误消息、位置和堆栈。 - 用
unhandledrejection事件补上 Promise 未捕获异常这个盲区。 - 用捕获阶段的
error事件监听捕获不冒泡的资源加载错误。 - 把三类错误统一封装、统一上报,形成一个能直接放进项目的采集模块。
🧱 JS 运行时错误:window.onerror
try/catch 只能处理可以预见的风险,不可能包住所有代码。全局兜底靠 window.onerror,任何没被捕获的运行时错误都会汇总到这里:
window.onerror = function (message, source, lineno, colno, error) {
console.log('错误消息:', message); // 例如 "Cannot read properties of undefined"
console.log('出错文件:', source); // 例如 "https://example.com/app.js"
console.log('出错位置:', lineno, colno); // 行号和列号
console.log('错误堆栈:', error.stack); // 完整的调用栈信息
// 返回 true 表示错误已被处理,浏览器不再在控制台输出红色报错
// 一般不建议返回 true,保留控制台报错方便本地排查
};
五个参数中排查时主要靠 error.stack:消息、文件、行号只说明错误发生在哪,堆栈能还原出错误的完整调用过程。
也可以用 window.addEventListener('error', handler) 监听,效果等价且支持挂多个监听器。本文后面统一用这种写法,因为它还能兼顾资源加载错误。
window.onerror 拿到的可能只有一句没有信息量的「Script error.」。解决办法是给 script 标签加 crossorigin 属性,同时 CDN 服务器要返回允许跨域的响应头。🧱 Promise 未捕获异常:unhandledrejection
Promise 里没人接住的错误不会触发 window.onerror,只会在控制台悄悄出现一条警告:
// 没有 catch,请求失败后的错误会"静默"掉
// async/await 写法同理,await 那行没有 try/catch 包住也一样
fetch('/api/user').then((res) => res.json());
捕获它要走专用通道:unhandledrejection 事件。
window.addEventListener('unhandledrejection', (event) => {
// event.reason 是 Promise 被拒绝的原因
console.log('Promise 未捕获异常:', event.reason);
});
注意 event.reason 不一定是 Error 对象,reject('网络超时') 传个字符串也合法。采集时要做兼容:是 Error 就取 stack,不是就转成字符串记录。
.catch() 或 try/catch 的 Promise 不会触发这个事件,它只抓「漏网之鱼」。日常该 catch 的照常 catch,全局监听只是兜底。🧱 资源加载错误:捕获阶段的 error 事件
图片 404、JS 下载失败这类资源加载错误不会中断页面,但会导致样式错乱、功能缺失。捕获它有个坑:资源加载失败触发的 error 事件不冒泡,给 window 挂普通监听收不到。
理解解决办法前,先补一点事件传播的背景。事件在 DOM 中的传播分两个阶段:捕获阶段从 window 一路向下到达目标元素,冒泡阶段再从目标元素向上回到 window,平常的监听默认在冒泡阶段触发。资源加载错误虽然不「上楼」,但事件发生前仍要「下楼」到达目标,所以让监听器在捕获阶段工作就能截住它:给 addEventListener 传第三个参数 true。
还有一个区分问题:捕获阶段的 error 监听器也能收到运行时错误,要用 event.target 区分:运行时错误的 target 是 window,资源加载错误的 target 是具体元素(img、script、link 等),可从它身上取 src 或 href。完整写法见下一节的统一封装。
🧰 统一封装与上报
把三类错误的捕获组装成一个模块,应用入口引入一次即可全局生效:
// 上报函数:把错误信息发送到自己的服务器
function report(data) {
// navigator.sendBeacon 适合上报场景:
// 即使页面正在关闭,浏览器也会尽量把数据发出去
const body = JSON.stringify(data);
navigator.sendBeacon('/api/monitor', body);
}
// 运行时错误 + 资源加载错误(捕获阶段监听,用 target 区分)
window.addEventListener(
'error',
(event) => {
if (event.target && event.target !== window) {
// 资源加载错误
report({
type: 'resource',
url: event.target.src || event.target.href,
tag: event.target.tagName,
});
} else {
// JS 运行时错误
report({
type: 'runtime',
message: event.message,
file: event.filename,
line: event.lineno,
stack: event.error && event.error.stack,
});
}
},
true
);
// Promise 未捕获异常
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
report({
type: 'promise',
message: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : '',
});
});
import './monitor';
真实项目里还会给每条上报带上页面 URL、用户标识、时间戳,并对相同错误去重,避免一次刷新产生的几十条相同上报淹没服务器。
app.config.errorHandler 专门捕获组件渲染、生命周期里抛出的错误,与全局监听互补。Sentry 这类成熟工具也是在这些 API 之上做封装,本课程后面的 Sentry 接入实践会专门讲解。🧾 小节总结
window.onerror(或error事件监听)捕获 JS 运行时错误,排查时主要看error.stack堆栈。- Promise 未捕获异常走
unhandledrejection事件,window.onerror管不到;event.reason不一定是 Error,要做兼容。 - 资源加载错误不冒泡,要用捕获阶段监听(第三个参数传
true),并用event.target !== window和运行时错误区分。 - 三类错误统一封装成一个模块,入口引入一次即可全局生效;上报用
navigator.sendBeacon,页面关闭时也能尽量发出。 - 跨域脚本会让错误信息变成「Script error.」,需要
crossorigin属性和服务端响应头配合。
❓ 知识问答
Q1:有了 try/catch,为什么还要全局错误监听?
A:try/catch 只能包住你能预见风险的代码,不可能包住整个应用。全局监听是兜底,专抓意料之外的错误,两者是配合关系。
Q2:try/catch 能捕获 Promise 里的错误吗?
A:分写法。promise.then() 的回调包不住,要靠 .catch() 或 unhandledrejection;async/await 中 await 那一行包在 try/catch 里可以捕获,因为它把异步「等成了同步」。
Q3:资源加载错误为什么不冒泡?
A:这是 DOM 规范的设计:加载失败属于资源自身的状态变化,和它所在的 DOM 位置无关,没必要通知祖先元素。但事件传播前仍会经过捕获阶段到达目标,所以捕获阶段监听可以截住它。
Q4:为什么上报用 sendBeacon 而不是 fetch?
A:错误常发生在用户关闭页面的瞬间,这时 fetch 很可能被取消。sendBeacon 专为「页面即将卸载也要把数据发出去」设计,缺点是不支持读取响应、数据量有上限。
Q5:线上代码是压缩混淆过的,上报的行列号和堆栈还有用吗?
A:直接看没什么用。要配合打包时生成的 sourcemap 文件做还原,把混淆后的位置映射回源码位置。手动还原比较繁琐,Sentry 可以自动完成,后续文章会讲到。
🧪 小练习
练习一:下面的采集代码有两个问题,导致资源加载错误收不到、且两类错误混在一起分不清,请修正:
window.addEventListener('error', (event) => {
// 请在这里编写代码:区分运行时错误和资源加载错误,并分别打印
});
提示:想想第三个参数该传什么,以及用哪个属性区分两类错误。
练习二:给下面的 Promise 错误采集补上对非 Error 原因的兼容处理,保证 reject('网络超时') 这样的字符串也能被正确记录:
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
// 请在这里编写代码:reason 是 Error 时记录 message 和 stack,否则记录字符串本身
});
🎉 恭喜你已经掌握错误采集技能啦!下一篇我们进入埋点与数据上报,看看用户行为数据是怎么收集的。
