如何监控前端异常
一套完整的前端异常监控体系建设复杂度很高,涵盖异常捕获、数据收集、归类统计、分析告警、现场复现、源码定位,同时还要考量日志存储、上报量对服务端的压力、采样策略等工程问题。
本文聚焦浏览器端异常捕获与上报:先分清要盯哪些异常,再落到能用的捕获 API、上报通道和常见坑。
为什么要监控前端异常?
- 提升用户体验:提前感知线上故障,避免大量用户遇到报错却没有反馈渠道。
- 远程定位线上问题:不需要复现用户环境,通过上报日志还原故障现场。
- 提前发现潜在缺陷:版本上线后快速捕捉隐性 bug,不等用户大量投诉。
- 解决移动端复现难痛点:移动端机型、系统、WebView 环境繁杂,很多问题本地无法复现。
- 完善工程体系:异常监控是前端工程化、质量保障体系中必不可少的一环。
JS 发生异常不会直接让 JS 引擎整体崩溃,只会终止当前执行任务,其余代码依旧可以运行;但网页崩溃属于渲染进程整体挂掉,JS 完全停止执行,需要单独方案监控。
需要监控哪些前端异常?
JS 代码异常
- 语法错误 SyntaxError:代码书写语法非法,脚本直接解析失败。
- 运行时异常
EvalError:规范里与eval相关的错误类;现行引擎里eval非法代码通常抛SyntaxError,业务里很少再见到EvalErrorRangeError:数值超出合法范围ReferenceError:引用未定义变量TypeError:变量类型错误URIError:URI 处理函数传参非法Error:通用基础错误类
静态资源加载异常
img、script、link、audio、video、iframe、@font-face 等外链资源加载失败(404、跨域、网络中断)。
Promise 未捕获异常
Promise 内部 reject,但是没有写 .catch() 进行捕获处理。
网络请求异常
XMLHttpRequest请求失败(超时、4xx、5xx)fetch请求异常(fetch只会在网络层失败才reject,HTTP 错误码如 404、500 不会抛出异常)
网页崩溃异常
浏览器渲染进程崩溃,页面白屏,所有 JS 停止执行,普通异常捕获 API 全部失效,需要特殊方案检测。
有哪些捕获前端异常的方式?
try-catch-finally
只能捕获同步运行时错误,无法捕获语法错误、异步错误(定时器、回调、Promise)。适合对可疑业务代码块做局部保护。
try {
throw new Error("业务代码执行出错");
} catch(e){
// 捕获错误对象,做上报处理
report(e);
} finally {
console.log("无论是否报错都会执行");
}
window.onerror
全局监听 JS 运行时错误,包括部分语法错误。
限制:不能捕获资源加载异常、网络请求异常。
注意:该监听代码必须优先于业务脚本执行;返回 true 可以阻止浏览器控制台打印未捕获错误。
window.onerror = function (msg, url, row, col, error) {
console.log({ msg, url, row, col, error });
return true; // 阻止控制台抛出
};
参数:
msg:错误信息;url:报错脚本地址;row:发生错误的行号;col:发生错误的列号;error:完整 Error 对象(包含堆栈 stack)。
window.addEventListener
资源的 error 事件不会向上冒泡,需要开启捕获阶段监听。
- 可以捕获:JS 运行时错误 + 静态资源加载异常(script、img、link 加载失败)
- 不能识别 HTTP 状态码,无法区分 404、500,需要结合事件的
srcElement判断元素类型。
注意:onerror 和 addEventListener 都会收到 JS 运行时错误,需要做判断,避免同一错误重复上报;只有 event.srcElement 是资源 DOM 元素时,才判定为资源加载错误。
window.addEventListener('error', function(event) {
// 判断是资源加载异常
if (event.srcElement && (
event.srcElement instanceof HTMLScriptElement ||
event.srcElement instanceof HTMLLinkElement ||
event.srcElement instanceof HTMLImageElement
)) {
console.log("资源加载失败:", event.srcElement.src);
}
}, true)
unhandledrejection(Promise 全局异常)
没有 catch 的 Promise 抛出异常不会触发 onerror,需要监听 unhandledrejection 事件。调用 e.preventDefault() 可以阻止控制台打印 Uncaught (in promise) 警告。
window.addEventListener("unhandledrejection", function(e){
console.log("Promise未捕获异常", e.reason, e.promise);
e.preventDefault();
})
Promise.catch
代码中 Promise 务必添加 .catch() 做本地捕获,避免异常抛到全局。
new Promise((resolve, reject) => {
throw new Error("promise内部报错");
}).catch(e => {
console.log("捕获promise异常", e);
})
拦截网络请求
原生 onerror 无法捕获接口返回 404、500 这类业务 HTTP 错误,需要劫持原生对象(重写 XMLHttpRequest、fetch),拦截请求的失败、超时、abort 场景。
fetch 特性:HTTP 非 2xx 状态不会 reject,只有网络完全失败才会走 catch,因此需要手动判断 response.ok。
// 劫持XMLHttpRequest
function hookXhr(report) {
if (!window.XMLHttpRequest) return;
const originSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
const handle = (e) => {
if(this.readyState === 4 && this.status >=400) {
report({
type: "xhr_error",
status: this.status,
url:this.responseURL
})
}
}
this.addEventListener('error', handle);
this.addEventListener('load', handle);
this.addEventListener('abort', handle);
return originSend.apply(this, arguments);
}
}
// 劫持fetch
function hookFetch(report) {
if(!window.fetch) return;
const originFetch = window.fetch;
window.fetch = function(...args) {
return originFetch.apply(this, args).then(res=>{
if(!res.ok) {
report({
type: "fetch_error",
url: args[0],
status:res.status
})
}
return res;
}).catch(err=>{
report({type:"fetch_network_error", err})
throw err;
})
}
}
框架专属异常钩子
Vue 2 Vue.config.errorHandler:捕获 Vue 组件渲染、生命周期内抛出的异常。
Vue.config.errorHandler = (err, vm, info) => {
console.error("Vue组件异常", err, vm, info);
}
Vue 3 app.config.errorHandler:挂到应用实例上,而不是全局 Vue.config。
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
console.error("Vue组件异常", err, instance, info);
}
componentDidCatch、Error Boundary:React 用 Error Boundary 捕获子组件渲染异常;类组件的 componentDidCatch 拿到错误信息。函数组件需要自行封装或使用支持 Error Boundary 的写法。
网页崩溃监控
网页崩溃时整个渲染进程停止,JS 全部停止执行,普通捕获 API 完全失效。
load + beforeunload + sessionStorage
原理:页面正常打开标记状态 pending;正常关闭触发 beforeunload 标记为 true;如果发生崩溃,页面直接销毁,不会执行 beforeunload。下次重新打开页面读取 sessionStorage,如果状态还是 pending,判定上一次发生崩溃。
缺陷:浏览器彻底关闭时 sessionStorage 会清空,会丢失崩溃标记;多标签页打开会造成误报。
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
// 页面再次加载后判断
if(sessionStorage.getItem('good_exit') && sessionStorage.getItem('good_exit') !== 'true') {
// 上报崩溃日志
console.log("检测到上一次页面发生崩溃", sessionStorage.getItem('time_before_crash'));
}
Service Worker 心跳检测(更可靠)
Service Worker 运行在独立线程,页面崩溃 SW 通常不受影响:
- 页面每隔一段时间向 SW 发送心跳,携带唯一 sessionId;
- 页面正常关闭发送 unload 消息,SW 清除该会话;
- SW 定时遍历会话列表,如果超过阈值没有收到心跳,则判定页面发生崩溃。
限制:SW 需要 HTTPS 环境,必须同域名注册;部分老旧浏览器不兼容。
异常信息上报方式
上报需要注意:报错的时候本身网络可能很差,上报逻辑不能阻塞业务,尽量使用异步,避免产生新异常。
Image 打点上报(最兼容)
利用图片请求,不会阻塞页面,跨域无限制;数据放在 URL query 参数,适合小体积日志。
function reportByImg(log) {
const img = new Image();
img.src = `/api/report?data=${encodeURIComponent(JSON.stringify(log))}`;
}
异步接口上报(fetch、XMLHttpRequest)
POST 发送完整错误对象、堆栈、上下文信息;使用 sendBeacon,页面卸载时也可以完成上报。
navigator.sendBeacon:页面 unload 阶段优先使用,浏览器会在后台排队发送,不会阻塞页面关闭。
function reportByBeacon(log) {
const data = JSON.stringify(log);
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/report', data);
} else {
fetch('/api/report', {method:'POST', body:data, keepalive:true})
}
}
异常监控常见问题
Script error. 跨域脚本报错
当 JS 脚本放在 CDN 跨域域名,浏览器出于安全策略,会隐藏完整堆栈,只返回 Script error.。
修复两个条件缺一不可:
<script src="cdn/xxx.js" crossorigin>给 script 标签添加crossorigin属性- CDN 资源服务器返回 CORS 响应头
Access-Control-Allow-Origin
onerror、addEventListener('error') 有什么区别?
在 JavaScript 中,onerror 和 addEventListener('error') 都是用来捕获错误事件的,两者最核心的区别在于:捕获错误的能力不同。
window.onerror:能捕获 JS 运行时错误,不能捕获静态资源加载错误(如图片 404、脚本加载失败)。addEventListener('error'):能捕获资源加载错误,也能捕获 JS 运行时错误。
原因:资源加载错误不冒泡,到不了 window,所以 window.onerror 收不到;而 addEventListener 既可以监听 window,也可以监听具体元素,开启捕获阶段,所以可以捕获这两种错误。
推荐:绝大部分项目以 addEventListener 为主即可。
只有一种边缘场景是 addEventListener 做不到的:阻止浏览器在控制台打印红色错误信息。
// 控制台不显示红色报错
window.onerror = function() {
return true;
};
// 无法阻止控制台不显示红色报错
window.addEventListener('error', function(e) {
e.preventDefault();
return false
});
监控 SDK 采集运行时错误时,以 addEventListener('error') 为主即可覆盖 JS 异常和资源加载失败;需要屏蔽控制台红色报错时,才额外使用 window.onerror 并 return true。
上报量过大压垮服务器
- 线上不建议全量上报,需要做采样率;
- 过滤开发环境错误;
- 过滤高频重复报错;
- 对上报队列做合并、限流。
压缩代码无法定位源码
上报得到的是压缩混淆后的堆栈,需要上传 sourcemap,监控平台通过 sourcemap 还原原始源码位置。
业界成熟监控工具
- Sentry(开源,推荐):支持多语言多框架,捕获 JS 异常、崩溃、性能指标,支持 sourcemap 解析、告警、issue 管理,可以自己 Docker 私有化部署,也可以直接使用 Sentry 云服务。
- 阿里 ARMS:阿里云前端监控 SaaS 产品。
- Fundebug:专注 JS 错误监控的 SaaS 平台。
- BetterJS:百度早期开源的轻量前端监控库,已停止活跃维护,新项目不建议作为首选。
Sentry 快速接入示例(Vue 3)
import * as Sentry from "@sentry/vue";
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
Sentry.init({
app,
dsn: "你的项目DSN地址",
tracesSampleRate: 0.2, // 性能采样率,生产环境不要设置 1.0
});
// 手动上报异常
try {
console.log(a);
} catch (err) {
Sentry.captureException(err);
}
// 上报自定义消息
Sentry.captureMessage("自定义告警消息");
Vue 2 把 app 换成根实例构造函数即可:Sentry.init({ Vue, dsn: "..." })。
总结
日志上报优先使用 sendBeacon 或图片打点;生产环境配置采样率,接入 sourcemap 还原混淆代码堆栈。
| 场景 | 捕获手段 |
|---|---|
| 局部可疑代码 | try-catch |
| 全局 JS 运行时异常 | window.onerror 或 addEventListener('error') |
| 静态资源加载失败 | addEventListener('error', ..., true) |
| 未捕获 Promise 异常 | unhandledrejection |
| XHR、fetch 接口错误 | 劫持重写原生请求对象 |
| Vue 组件内错误 | Vue 2:Vue.config.errorHandler;Vue 3:app.config.errorHandler |
| React 组件渲染错误 | Error Boundary |
| 页面崩溃 | beforeunload + sessionStorage,或 Service Worker 心跳 |