前端监控简介

错误采集

掌握 JS 运行时错误、Promise 异常与资源加载错误的采集方法,让线上报错无处可藏。

🎯 引言

本地开发时我们盯着控制台,报错一目了然;代码到了用户的浏览器里,报了什么错、错在哪一行,我们完全看不到。错误采集就是在用户浏览器里全局监听错误并自动上报给服务器,让线上报错有据可查。

学完本篇,你将能够:

  • window.onerror 捕获 JS 运行时错误,拿到错误消息、位置和堆栈。
  • unhandledrejection 事件补上 Promise 未捕获异常这个盲区。
  • 用捕获阶段的 error 事件监听捕获不冒泡的资源加载错误。
  • 把三类错误统一封装、统一上报,形成一个能直接放进项目的采集模块。

🧱 JS 运行时错误:window.onerror

try/catch 只能处理可以预见的风险,不可能包住所有代码。全局兜底靠 window.onerror,任何没被捕获的运行时错误都会汇总到这里:

monitor.js
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) 监听,效果等价且支持挂多个监听器。本文后面统一用这种写法,因为它还能兼顾资源加载错误。

如果 JS 文件部署在 CDN 等跨域域名下,且没有配置跨域,window.onerror 拿到的可能只有一句没有信息量的「Script error.」。解决办法是给 script 标签加 crossorigin 属性,同时 CDN 服务器要返回允许跨域的响应头。

🧱 Promise 未捕获异常:unhandledrejection

Promise 里没人接住的错误不会触发 window.onerror,只会在控制台悄悄出现一条警告:

demo.js
// 没有 catch,请求失败后的错误会"静默"掉
// async/await 写法同理,await 那行没有 try/catch 包住也一样
fetch('/api/user').then((res) => res.json());

捕获它要走专用通道:unhandledrejection 事件。

monitor.js
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 等),可从它身上取 srchref。完整写法见下一节的统一封装。


🧰 统一封装与上报

把三类错误的捕获组装成一个模块,应用入口引入一次即可全局生效:

monitor.js
// 上报函数:把错误信息发送到自己的服务器
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 : '',
    });
});
main.js
import './monitor';

真实项目里还会给每条上报带上页面 URL、用户标识、时间戳,并对相同错误去重,避免一次刷新产生的几十条相同上报淹没服务器。

Vue 还提供了 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()unhandledrejectionasync/awaitawait 那一行包在 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,否则记录字符串本身
});

🎉 恭喜你已经掌握错误采集技能啦!下一篇我们进入埋点与数据上报,看看用户行为数据是怎么收集的。