前端错误监控实战:从捕获到告警的完整方案
上线后用户报错,但控制台干干净净?
上周项目刚上线,测试同学说“一切正常”,结果第二天运营就甩来一张截图:用户点了个按钮,页面白屏了。我赶紧打开 DevTools,刷新页面,啥也没有——控制台清清爽爽,连个 warning 都没有。但用户那边就是炸了。这种“你本地跑得欢,线上死得惨”的情况,真让人抓狂。
一开始我以为是资源加载失败,比如 JS 文件 404 了。但查了 CDN 日志,所有静态资源都 200。后来又怀疑是某个第三方 SDK 报错被吞了,但把所有可疑的 script 标签一个个注释掉,问题依旧。折腾了半天,突然意识到:**有些错误根本不会在控制台打印出来**,尤其是异步回调、Promise reject、或者跨域脚本里的错误。这时候我才想起来,我们压根没做全局错误监控!
先上 window.onerror,结果只抓到一半
赶紧补救。最简单的办法当然是监听 window.onerror。我立马加了几行代码:
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到错误:', { message, source, lineno, colno, error });
// 这里应该上报到后端
return true; // 阻止默认错误处理(比如控制台打印)
};
本地测试了一下,同步错误确实能抓到。比如故意写个 undefinedVar.foo(),马上就能收到日志。但当我用 setTimeout 包一下,或者用 fetch 模拟网络错误时,onerror 完全没反应!
查了 MDN 才想起来:window.onerror 对 Promise reject 是无能为力的。而且如果错误发生在跨域脚本里(比如从 CDN 加载的 vendor.js),出于安全限制,拿到的 message 可能只是 “Script error.”,连具体哪行出错都不知道。这坑我以前踩过,但这次又忘了。
Promise 错误怎么破?得靠 unhandledrejection
既然 Promise 的 reject 没被捕获会静默失败,那必须单独监听。好在现代浏览器提供了 unhandledrejection 事件:
window.addEventListener('unhandledrejection', event => {
console.log('未处理的 Promise 拒绝:', event.reason);
// 上报逻辑
event.preventDefault(); // 阻止默认行为(比如控制台警告)
});
加上这段后,Promise 相关的错误终于能捕获了。但问题又来了:有些错误既不是同步抛出,也不是顶层 Promise reject,而是发生在 Vue 或 React 组件内部。比如 Vue 里某个方法调用了一个不存在的函数,这种错误会被框架自己的错误处理机制吃掉,根本不会冒泡到 window 层。
框架层的错误,得进框架内部捞
我们项目用的是 Vue 2。Vue 提供了全局的 Vue.config.errorHandler,专门用来捕获组件渲染和 watcher 中的错误。赶紧补上:
import Vue from 'vue';
Vue.config.errorHandler = (err, vm, info) => {
console.log('Vue 组件错误:', { err, info });
// 上报
};
React 的话就得用 Error Boundary 了,不过那是组件级别的,得在顶层包一层。但不管怎样,**不同环境的错误得用不同的钩子去抓**,这点让我有点烦躁——能不能统一处理?
核心代码就这几行:整合所有错误源
最后我搞了个简单的错误监控模块,把能想到的错误源都兜住。代码其实不复杂,关键是要覆盖全面:
// errorMonitor.js
function reportError(errorInfo) {
// 这里替换成你自己的上报接口
fetch('https://jztheme.com/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
}).catch(() => {
// 上报失败也别让错误扩散
});
}
// 1. 全局同步错误
window.onerror = function(message, source, lineno, colno, error) {
const isScriptError = message === 'Script error.' || message === '';
if (isScriptError) {
// 跨域脚本错误,信息太少,但至少记录一下
reportError({
type: 'script-error',
message: 'External script error',
url: source,
line: lineno,
col: colno
});
} else {
reportError({
type: 'js-error',
message,
stack: error?.stack || '',
url: source,
line: lineno,
col: colno
});
}
return true;
};
// 2. 未处理的 Promise reject
window.addEventListener('unhandledrejection', event => {
reportError({
type: 'unhandled-promise',
reason: event.reason?.toString() || 'Unknown reason',
stack: event.reason?.stack || ''
});
event.preventDefault();
});
// 3. Vue 错误(根据项目框架调整)
if (typeof Vue !== 'undefined') {
Vue.config.errorHandler = (err, vm, info) => {
reportError({
type: 'vue-error',
message: err.message,
stack: err.stack,
component: vm?.$options?.name || 'unknown',
info
});
};
}
这段代码我放在入口文件最顶部,确保尽早执行。亲测有效:现在无论是同步错误、异步错误、Promise 拒绝还是 Vue 组件崩了,都能上报到后端。后台看板一目了然,再也不用靠用户截图猜错了。
踩坑提醒:这三点一定注意
- 跨域脚本的错误信息是残缺的。如果你的 JS 打包后部署在 CDN,记得给 script 标签加上
crossorigin="anonymous",同时服务器要返回Access-Control-Allow-Origin头。不然onerror里只能拿到 “Script error.”,毫无用处。 - 别在错误处理函数里再抛错。比如
reportError里用了fetch,如果 fetch 本身出错(比如网络断了),千万别让它抛出去,否则可能陷入无限循环。所以我在fetch外面包了.catch(() => {})。 - Vue 的 errorHandler 只在生产模式生效。开发环境下 Vue 会直接把错误 throw 出来,方便调试。所以测试错误上报时,一定要用 production build。
改完之后,大部分错误都能捕获了。不过还有个小问题:某些极端情况下的内存泄漏错误(比如递归爆栈)可能连 onerror 都来不及触发。但这属于极少数场景,目前没遇到,暂时不管了。
不是最优解,但够用
其实更专业的做法是用 Sentry 或者自研一套完整的监控系统,带 source map 解析、错误聚合、用户行为回溯等功能。但我们小团队资源有限,这个轻量级方案已经能解决 90% 的问题了。毕竟,能快速定位线上问题是第一位的,没必要一上来就搞重武器。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如怎么优雅地处理 React 的 Error Boundary 上报,或者如何避免重复上报相同错误,我都想听听你的经验。

暂无评论