> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/research/02-ru-he-jian-kong-qian-duan-yi-chang.md).

# 如何监控前端异常？

一套完整的前端异常监控体系建设复杂度很高，涵盖**异常捕获、数据收集、归类统计、分析告警、现场复现、源码定位**，同时还要考量日志存储、上报量对服务端的压力、采样策略等工程问题。

本文聚焦浏览器端**异常捕获与上报**：先分清要盯哪些异常，再落到能用的捕获 API、上报通道和常见坑。

## 为什么要监控前端异常？

* **提升用户体验**：提前感知线上故障，避免大量用户遇到报错却没有反馈渠道。
* **远程定位线上问题**：不需要复现用户环境，通过上报日志还原故障现场。
* **提前发现潜在缺陷**：版本上线后快速捕捉隐性 bug，不等用户大量投诉。
* **解决移动端复现难痛点**：移动端机型、系统、WebView 环境繁杂，很多问题本地无法复现。
* **完善工程体系**：异常监控是前端工程化、质量保障体系中必不可少的一环。

JS 发生异常不会直接让 JS 引擎整体崩溃，只会终止**当前执行任务**，其余代码依旧可以运行；但网页崩溃属于渲染进程整体挂掉，JS 完全停止执行，需要单独方案监控。

## 需要监控哪些前端异常？

### JS 代码异常

* **语法错误 SyntaxError**：代码书写语法非法，脚本直接解析失败。
* **运行时异常**
  * `EvalError`：规范里与 `eval` 相关的错误类；现行引擎里 `eval` 非法代码通常抛 `SyntaxError`，业务里很少再见到 `EvalError`
  * `RangeError`：数值超出合法范围
  * `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）**。适合对可疑业务代码块做局部保护。

```javascript
try {
  throw new Error("业务代码执行出错");
} catch(e){
  // 捕获错误对象，做上报处理
  report(e);
} finally {
  console.log("无论是否报错都会执行");
}
```

### window\.onerror

全局监听 JS 运行时错误，包括部分语法错误。

限制：**不能捕获资源加载异常、网络请求异常**。

**注意**：该监听代码必须优先于业务脚本执行；返回 `true` 可以阻止浏览器控制台打印未捕获错误。

```javascript
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 元素时，才判定为资源加载错误。

```javascript
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)` 警告。

```javascript
window.addEventListener("unhandledrejection", function(e){
  console.log("Promise未捕获异常", e.reason, e.promise);
  e.preventDefault();
})
```

### Promise.catch

代码中 Promise 务必添加 `.catch()` 做本地捕获，避免异常抛到全局。

```javascript
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`。

```javascript
// 劫持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);
  }
}
```

```javascript
// 劫持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 组件渲染、生命周期内抛出的异常。

```javascript
Vue.config.errorHandler = (err, vm, info) => {
  console.error("Vue组件异常", err, vm, info);
}
```

**Vue 3 `app.config.errorHandler`**：挂到应用实例上，而不是全局 `Vue.config`。

```javascript
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` 会清空，会丢失崩溃标记；多标签页打开会造成误报。

```javascript
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 参数，适合小体积日志。

```javascript
function reportByImg(log) {
  const img = new Image();
  img.src = `/api/report?data=${encodeURIComponent(JSON.stringify(log))}`;
}
```

### 异步接口上报（fetch、XMLHttpRequest）

POST 发送完整错误对象、堆栈、上下文信息；使用 `sendBeacon`，页面卸载时也可以完成上报。

**`navigator.sendBeacon`**：页面 unload 阶段优先使用，浏览器会在后台排队发送，不会阻塞页面关闭。

```javascript
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` 做不到的：**阻止浏览器在控制台打印红色错误信息**。

```javascript
// 控制台不显示红色报错
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）

```javascript
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 心跳           |
