学完这篇,你能亲手写出一套同时兜住同步报错、资源加载失败和 Promise 未捕获拒绝的前端错误监控上报代码。
第一步:先分清错误从哪个口子出来
前端错误大致分三类,对应两个 API:
- 同步运行时错误(`undefined is not a function` 这种)→ `window.onerror`
- 资源加载失败(图片、脚本、样式 404)→ 只能用捕获阶段的 `error` 事件
- Promise 里报错但没写 `.catch()` → `unhandledrejection`
很多人只挂了 `window.onerror`,结果线上 Promise 报错一条都收不到,这是最常见的漏报点。
第二步:写对 window.onerror
window.onerror = function (message, source, lineno, colno, error) {
report({
type: 'error',
message: message,
stack: error && error.stack, // 关键:堆栈在第五个参数里
file: source,
line: lineno,
col: colno,
});
return false; // 返回 true 会吃掉控制台的默认报错,调试期不建议
};
注意:`window.onerror` 是赋值式的,页面里只有一个。如果你用的框架或第三方 SDK 也挂了它,后挂的会覆盖前面的。要共存就改成 `window.addEventListener('error', handler)`,这样能挂多个。
注意:`stack` 一定要从第五个参数 `error` 上取,直接用 `message` 是拿不到调用栈的。
第三步:资源加载错误要单独监听
图片、`<script>`、`<link>` 加载失败不会走 `window.onerror`,因为资源错误不冒泡。必须在捕获阶段截:
window.addEventListener('error', function (e) {
const target = e.target;
if (target && (target.src || target.href)) {
report({
type: 'resource',
tag: target.tagName,
url: target.src || target.href,
});
}
}, true); // 第三个参数 true 不能省
注意:这个监听器里 `e.message` 是空的,判断是不是资源错误就看 `e.target` 有没有 `src`/`href`。
第四步:补上 unhandledrejection
window.addEventListener('unhandledrejection', function (e) {
const reason = e.reason;
report({
type: 'promise',
message: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : '',
});
// e.preventDefault(); // 需要的话可以压掉控制台红字
});
注意:`e.reason` 不一定是 Error。有人 `Promise.reject('出错了')`、`Promise.reject({code: 500})`,直接取 `.stack` 会得到 `undefined`。上面这段做了归一化,别偷懒。
如果项目里用了 axios/fetch 封装,建议在请求拦截器里再兜一层,Promise 里的网络错误往往被业务代码吞掉了。
第五步:上报本身别把页面搞挂
推荐用 `navigator.sendBeacon`,它在页面卸载时也能发出请求:
function report(payload) {
payload.url = location.href;
payload.ts = Date.now();
payload.ua = navigator.userAgent;
try {
const data = JSON.stringify(payload);
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/log/error', data);
} else {
new Image().src = '/api/log/error?d=' + encodeURIComponent(data);
}
} catch (err) {
// 上报失败绝不能再抛错,否则死循环
}
}
注意:`sendBeacon` 有体积限制(约 64KB),别把整个 DOM 快照塞进去。同时要做去重和采样——同一个错误短时间刷几百条,后端会被打爆。常规做法是「错误信息 + 行号」做 key,同 key 每分钟只报一次。
第六步:让堆栈能看懂
线上代码是压缩过的,`a.js:1:23456` 这种行号没法定位。两个办法:
- 打包时开启 sourcemap,上报后到 Sentry 这类平台或自建服务还原;
- 引用第三方脚本时加 `crossorigin="anonymous"`,并且 CDN 返回 `Access-Control-Allow-Origin`,否则你只会收到一条 `Script error.`,什么信息都没有。
注意:`Script error.` 基本就是跨域脚本没配 CORS,不是代码本身的问题。
另外,框架有自己的错误出口:Vue 是 `app.config.errorHandler`,React 是 `ErrorBoundary` / `componentDidCatch`。它们能捕获组件渲染期的错误,但不会自动进你的 `window.onerror` 上报,需要手动转发一次。
小结
- 同步错误用 `window.onerror`,堆栈从第五个参数取
- 资源错误用 `addEventListener('error', fn, true)`,捕获阶段才拦得到
- Promise 错误用 `unhandledrejection`,`reason` 要归一化
- 上报用 `sendBeacon`,并且整段包 `try/catch`,绝不允许上报自己报错
- 必做去重、采样、限流,否则监控系统先崩
- 跨域脚本加 `crossorigin`,否则只有 `Script error.`
- 接上 sourcemap 还原,堆栈才有意义