前端错误监控与异常处理
线上出错永远是前端最头疼的事:用户反馈”页面白了”,你打开 F12 什么都没有;日志里有一行 Script error,无从下手。
建立一套完善的前端错误监控体系,可以让你在用户之前发现问题,并提供足够的上下文信息来快速定位根因。本文从错误分类、捕获方式、上报策略到 Source Map 还原,带你系统了解前端错误监控的全貌。
前端错误分类
1. JavaScript 运行时错误
// TypeError
null.toString(); // Cannot read properties of null
// ReferenceError
console.log(undeclared); // undeclared is not defined
// SyntaxError
JSON.parse("invalid"); // Unexpected token i
// RangeError
new Array(-1); // Invalid array length
2. Promise 未捕获异常
// 未 catch 的 Promise rejection
fetch("/api/data").then((res) => {
throw new Error("处理失败"); // 没有 .catch(),变成 unhandledRejection
});
async function loadData() {
const data = await fetchData(); // 可能抛出,但调用处没有 try/catch
}
loadData(); // unhandledRejection
3. 资源加载错误
<!-- 图片 404 -->
<img src="/not-found.png" />
<!-- 脚本加载失败 -->
<script src="https://cdn.example.com/lib.js"></script>
4. 跨域脚本错误(Script error)
当页面引入了来自其他域名的 JS,该脚本抛出错误时,出于安全原因,浏览器只会显示 Script error,不暴露具体信息。
5. 接口请求异常
HTTP 4xx/5xx、超时、网络断开等,通常在业务层捕获但容易遗漏统一记录。
错误捕获方式
try/catch:捕获同步错误
try {
JSON.parse(rawData);
processData(data);
} catch (error) {
// error.name: 错误类型
// error.message: 错误信息
// error.stack: 调用栈
reportError(error);
}
注意:try/catch 无法捕获异步错误(setTimeout 内的错误、Promise rejection)。
window.onerror:全局 JS 错误兜底
window.onerror = function (message, source, lineno, colno, error) {
// message: 错误信息
// source: 出错脚本 URL
// lineno/colno: 行列号
// error: Error 对象(含 stack)
reportError({
type: "js",
message,
source,
position: `${lineno}:${colno}`,
stack: error?.stack,
});
return true; // 返回 true 阻止错误向上传播(不显示在控制台)
};
addEventListener(‘error’):资源加载错误
window.onerror 无法捕获资源加载失败,需要用捕获阶段的事件监听:
window.addEventListener(
"error",
(event) => {
const target = event.target;
// 区分资源错误和 JS 错误
if (
target instanceof HTMLImageElement ||
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement
) {
reportError({
type: "resource",
tagName: target.tagName,
src: target.src || target.href,
message: `${target.tagName} 加载失败`,
});
}
},
true // ⚠️ 必须是捕获阶段
);
unhandledrejection:未处理的 Promise 异常
window.addEventListener("unhandledrejection", (event) => {
// event.promise: 出问题的 Promise
// event.reason: 拒绝原因(可能是 Error,也可能是字符串)
const reason = event.reason;
reportError({
type: "promise",
message: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
});
event.preventDefault(); // 阻止默认的控制台错误输出
});
跨域脚本:添加 crossorigin 属性
<!-- 允许跨域脚本暴露完整错误信息 -->
<script src="https://cdn.example.com/lib.js" crossorigin="anonymous"></script>
同时服务端需要返回 Access-Control-Allow-Origin: * 响应头。
封装统一的错误上报函数
const ErrorReporter = {
// 错误队列(用于合并上报)
queue: [],
timer: null,
// 上报单个错误
report(error) {
const errorInfo = {
type: error.type || "unknown",
message: error.message,
stack: error.stack,
// 用户和环境信息
url: location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
// 用户 ID(如果已登录)
userId: window.__USER_ID__ ?? "anonymous",
};
this.queue.push(errorInfo);
this.flush();
},
// 批量上报(合并请求,减少接口压力)
flush() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
if (this.queue.length === 0) return;
const errors = this.queue.splice(0); // 取出所有待上报数据
// 优先用 sendBeacon(页面关闭时也能发送)
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/errors", JSON.stringify(errors));
} else {
fetch("/api/errors", {
method: "POST",
body: JSON.stringify(errors),
headers: { "Content-Type": "application/json" },
keepalive: true, // 页面关闭后请求继续发送
});
}
}, 1000); // 1秒内的错误合并一次上报
},
};
为什么用 sendBeacon?
sendBeacon 是专为分析打点设计的 API,有两个关键优势:
- 页面关闭时依然能发出(XHR/fetch 在 unload 事件中经常被浏览器取消)
- 不阻塞页面卸载
// ❌ 页面关闭时可能发不出去
window.addEventListener("beforeunload", () => {
fetch("/api/report", { method: "POST", body: data });
});
// ✅ 可靠发送
window.addEventListener("beforeunload", () => {
navigator.sendBeacon("/api/report", data);
});
Source Map:还原压缩代码的真实位置
生产环境的 JS 经过压缩混淆,错误栈里的行列号是压缩后代码的位置,几乎无法定位。Source Map 能将压缩代码的位置映射回源码位置。
生成 Source Map
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // 生成 .map 文件
// 或 'hidden':生成 map 文件但不在 JS 里添加引用注释(推荐线上使用)
// sourcemap: 'hidden',
},
});
// webpack.config.js
module.exports = {
devtool: "hidden-source-map", // 只生成 map 文件,不暴露给用户
};
安全提示:不要将 Source Map 文件部署到公开可访问的位置,否则源码会被任何人还原。推荐上传到监控平台(如 Sentry),或放在内网服务器。
服务端还原错误位置
// Node.js 服务端:使用 source-map 库还原位置
import { SourceMapConsumer } from "source-map";
import fs from "fs";
async function resolveError(errorInfo) {
const { source, line, column } = errorInfo;
// 找到对应的 .map 文件
const mapFile = getMapFilePath(source);
const rawSourceMap = JSON.parse(fs.readFileSync(mapFile, "utf-8"));
await SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
const originalPosition = consumer.originalPositionFor({ line, column });
console.log(`
压缩后:${source}:${line}:${column}
源码位置:${originalPosition.source}:${originalPosition.line}:${originalPosition.column}
函数名:${originalPosition.name}
`);
});
}
推荐使用 Sentry
如果不想自建监控系统,Sentry 是最成熟的开源错误监控平台,几行代码接入,自动处理 Source Map 还原、错误聚合、告警通知。
npm install @sentry/react
// main.tsx
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "https://[email protected]/xxx",
environment: import.meta.env.MODE,
// 采样率:只上报 10% 的正常请求(性能监控),错误始终上报
tracesSampleRate: 0.1,
// 关联 Source Map(需在 CI 中上传 map 文件到 Sentry)
release: __APP_VERSION__,
});
// 主动捕获并上报错误(携带额外上下文)
try {
await submitOrder(orderData);
} catch (error) {
Sentry.withScope((scope) => {
scope.setTag("feature", "checkout");
scope.setExtra("orderData", orderData);
Sentry.captureException(error);
});
showErrorToast("下单失败,请重试");
}
从零搭建简易监控 SDK
如果想自研,以下是一个功能完整的最小化监控 SDK:
class Monitor {
constructor(options) {
this.options = { endpoint: "/api/errors", sampleRate: 1, ...options };
this.init();
}
init() {
// JS 运行时错误
window.onerror = (msg, src, line, col, error) => {
this.capture({
type: "js",
message: msg,
stack: error?.stack,
source: `${src}:${line}:${col}`,
});
};
// 未捕获的 Promise 异常
window.addEventListener("unhandledrejection", (e) => {
const reason = e.reason;
this.capture({
type: "promise",
message: reason?.message ?? String(reason),
stack: reason?.stack,
});
});
// 资源加载失败
window.addEventListener(
"error",
(e) => {
const t = e.target;
if (t instanceof HTMLElement && t !== window) {
this.capture({
type: "resource",
tag: t.tagName,
src: t.src || t.href,
});
}
},
true
);
}
capture(error) {
// 采样控制(减少上报量)
if (Math.random() > this.options.sampleRate) return;
const payload = {
...error,
url: location.href,
ts: Date.now(),
ua: navigator.userAgent,
};
navigator.sendBeacon(this.options.endpoint, JSON.stringify(payload));
}
}
// 初始化
new Monitor({
endpoint: "https://monitor.example.com/collect",
sampleRate: 0.5,
});
总结
一套完整的前端错误监控体系包含以下层次:
捕获层
├── try/catch → 同步代码中主动捕获
├── window.onerror → JS 运行时兜底
├── unhandledrejection → Promise 异常
└── addEventListener('error', ..., true) → 资源加载失败
上报层
├── 合并批量上报(减少请求次数)
├── sendBeacon(可靠发送)
└── 采样率控制(降低服务器压力)
分析层
├── Source Map 还原(定位真实源码位置)
├── 错误聚合(相同错误合并计数)
└── 告警通知(邮件/钉钉/Slack)
即使业务早期没有精力搭建完整的监控平台,至少要接入 Sentry 或类似工具做基础的错误收集——线上无感知的 bug 是最危险的 bug。