公司那套内部系统最早只挂了一个 window.onerror,我以为够用了。2025 年底有个同事反馈说点某个按钮没反应,我查日志一条报错都没有,这才意识到它漏掉的东西比我想的多。补了几路监听之后,一周的上报量从三十来条涨到八十多条,其中大概一半是真问题。
一、window.onerror 抓不到什么
三类,我按发现的先后写:
- Promise 里抛出且没被 catch 的异常。这类走的是
unhandledrejection事件,window.onerror完全不知道。我们项目里最多的是接口请求失败之后在.then里继续取值,比如res.data.list.length,res 是 undefined 就炸了。 - 资源加载失败。
<img>、<script>、<link>加载 404 时触发的 error 事件不冒泡,所以挂在 window 上的普通监听收不到,必须在捕获阶段监听。 - Vue 组件内部的异常。Vue 内部有 try/catch,渲染和生命周期里抛的错会交给
app.config.errorHandler,默认行为是打一条 console.error,不会冒到 window.onerror。
另外还有一个经常被人忽略的:跨域脚本里的异常,浏览器出于安全考虑只给 Script error. 这一句,没有堆栈。要拿到真实信息得给 script 标签加 crossorigin="anonymous",并且 CDN 那边要回 Access-Control-Allow-Origin。我们有一个第三方图表库是跨域加载的,这个问题到现在还有一小部分没解决,因为那个 CDN 不受我们控制。
二、怎么补
把三路监听合并到一个地方,用一个带捕获的 error 监听同时处理脚本错误和资源错误,靠 event.target 区分:
function isResourceError(e) {
const t = e.target
return !!t && t !== window && !!(t.src || t.href)
}
window.addEventListener('error', (e) => {
if (isResourceError(e)) {
report({ type: 'resource', url: e.target.src || e.target.href })
return
}
report({
type: 'js',
message: e.message,
stack: e.error && e.error.stack,
})
}, true)
window.addEventListener('unhandledrejection', (e) => {
const r = e.reason
report({
type: 'promise',
message: r instanceof Error ? r.message : String(r),
stack: r && r.stack,
})
})
Vue 那边单独接一下,info 字段会告诉你是 render、setup 还是某个生命周期钩子里出的错,这个信息排查时很有用:
app.config.errorHandler = (err, instance, info) => {
report({
type: 'vue',
message: String(err),
info,
stack: err instanceof Error ? err.stack : undefined,
})
}
设置了 errorHandler 之后,Vue 就不会再把错误往外抛了,所以上面这两套监听是并行关系,不是二选一。另外开发环境下我建议仍然让它打到控制台,不然本地调试会少一大半信息。
三、补完之后要处理的两件事
第一件是去重。同一个用户的一处问题可能在一秒内触发几十次上报(比如渲染报错导致的死循环),我在 report 里加了个简单的 key 缓存,同一个 message 五秒内只报一次,只累加计数。这个阈值是我拍的,没有依据。
第二件是上报本身要能失败得安静。用户关页面时用 navigator.sendBeacon 比 fetch 可靠;上报接口挂了不能反过来影响页面,整个上报逻辑我包了一层 try/catch,失败了就扔掉。
还有 sourcemap:构建产物是压缩过的,堆栈里全是 index-a3f2c1.js:1:28473 这种,看不了。我们把 map 文件传到内网的一个目录里,排查时手动对着查,没有做自动解析——两个人的项目,这个成本还能接受。
四、值不值得做
我觉得值得,但要说清楚收益在哪:它没帮我们提前发现过重大事故,主要作用是把「用户说有问题但我们复现不了」变成有据可查。上面说的那个按钮没反应的问题,最后查出来是某个列表字段偶尔是 null,前端直接取了 .length,属于 Promise 异常,之前完全静默。
没做的部分也说一下:我们没买任何商业监控服务,就是自己往内部日志里写,采样比例是 100%,因为量小。如果哪天上报量大到影响性能,我会先加采样而不是先优化上报逻辑。