
一句话:前端错误有三类来源——React 渲染错误(用 ErrorBoundary)、全局 JS 运行时错误(
window.onerror/error事件)、未处理的 Promise 拒绝(unhandledrejection);三者都要接上,并做「去重、采样、源码映射、上下文补充、可靠上报」。
一、三类错误来源
| 来源 | 例子 | 捕获方式 |
|---|---|---|
| React 渲染错误 | 组件里对 undefined 取属性 |
Error Boundary(componentDidCatch) |
| 全局 JS 错误 | 事件处理里 throw、语法/运行时错误 |
window.onerror / window.addEventListener('error') |
| 未处理的 Promise | Promise.reject()、async 里 throw 未 catch |
window.addEventListener('unhandledrejection') |
| 资源加载失败 | 图片/脚本/样式 404 | window.addEventListener('error', handler, true)(捕获阶段) |
| 接口错误 | 4xx/5xx | 请求层拦截器(见 7.1) |
⭐ 关键认知:
· ErrorBoundary 只覆盖「渲染错误」(见 8.3)
· 事件处理、异步、资源加载的错误必须用「全局监听」
· 接口错误应该在「请求层」统一上报(带上接口与状态码上下文)
→ 完整的监控是「四者叠加」,不是「选一个」
二、Error Boundary 上报
class ErrorBoundary extends Component<Props, State> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
// ⭐ 这里能拿到「组件栈」——定位问题最关键的上下文
reportError(error, {
type: 'react-render',
componentStack: info.componentStack,
});
}
render() {
return this.state.error ? this.props.fallback : this.props.children;
}
}
⚠️ ErrorBoundary 上报的三个注意点:
① `info.componentStack` 是【定位 React 错误的核心】——它告诉你「哪条组件链抛的错」
② 要「去重」——StrictMode 下会捕获两次(错误对象相同)
③ 要「带上当前路由/用户/版本」等上下文(便于复现)
三、全局 JS 错误
// monitor/global-error.ts
export function setupGlobalErrorHandlers() {
// ① 全局运行时错误(含未捕获的同步错误)
window.addEventListener('error', (event) => {
// ⚠️ 资源加载失败也会触发这里(event.target 不是 window)
const target = event.target as HTMLElement | null;
if (target && target !== (window as unknown as HTMLElement) && (target as any).tagName) {
reportError(new Error(`资源加载失败: ${(target as any).src || (target as any).href}`), {
type: 'resource',
tagName: (target as any).tagName,
url: (target as any).src || (target as any).href,
});
return;
}
reportError(event.error ?? new Error(event.message), {
type: 'js-runtime',
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
}, true); // ⭐ 用捕获阶段,才能拿到「资源加载失败」
// ② 未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
const error = reason instanceof Error ? reason : new Error(String(reason));
reportError(error, { type: 'unhandled-rejection' });
// ⚠️ 是否调用 event.preventDefault() 取决于「是否要让控制台也报错」
});
// ③ (可选)把 React 的「已捕获但仍需上报」的错误也接上
}
// ⚠️ window.onerror 与 addEventListener('error') 的区别
window.onerror = (message, source, lineno, colno, error) => { /* ... */ };
// · 只能有一个(后者会覆盖前者)
// · 拿不到「资源加载失败」(因为没有 error 参数)
window.addEventListener('error', handler, true);
// · 可以有多个(不会被覆盖)
// · 用 true(捕获阶段)能拿到「资源加载失败」事件
// ✅ 推荐用 addEventListener
// ✅ 在应用入口挂上(并且只挂一次)
// main.tsx
setupGlobalErrorHandlers();
createRoot(root).render(
<StrictMode>
<ErrorBoundary onError={reportError}>
<App />
</StrictMode>
</StrictMode>
);
四、上报策略
1) 上下文(最重要的部分)
interface ErrorContext {
type: 'react-render' | 'js-runtime' | 'unhandled-rejection' | 'resource' | 'api';
componentStack?: string;
filename?: string;
lineno?: number;
colno?: number;
// ⭐ 业务上下文(对排查至关重要)
url?: string;
route?: string;
userId?: string;
appVersion?: string;
userAgent?: string;
release?: string;
// ⭐ 自定义(如最近的操作、接口请求 ID)
breadcrumbs?: Array<{ time: number; action: string; detail?: unknown }>;
}
// ✅ 用「面包屑」记录「用户做了什么」(错误发生前的操作序列极有价值)
const breadcrumbs: Breadcrumb[] = [];
export function addBreadcrumb(action: string, detail?: unknown) {
breadcrumbs.push({ time: Date.now(), action, detail });
if (breadcrumbs.length > 50) breadcrumbs.shift(); // ⭐ 只保留最近 50 条
}
// 在关键操作里埋点
addBreadcrumb('navigate', { to: location.pathname });
addBreadcrumb('api', { url, method, status });
addBreadcrumb('click', { selector: 'submit-order' });
2) 去重与限流
const reported = new Map<string, number>();
const DEDUPE_WINDOW = 5000; // 5 秒内相同错误只报一次
const MAX_PER_MINUTE = 20; // 每分钟最多上报 20 条
let minuteCount = 0;
let minuteStart = Date.now();
export function reportError(error: Error, context: ErrorContext) {
// ① 去重(用「消息 + 栈的前 N 行」做指纹)
const fingerprint = `${error.name}:${error.message}:${(error.stack ?? '').split('\n')[1] ?? ''}`;
const last = reported.get(fingerprint);
if (last && Date.now() - last < DEDUPE_WINDOW) return;
reported.set(fingerprint, Date.now());
// ② 限流(防止「循环报错」把监控打爆)
if (Date.now() - minuteStart > 60_000) {
minuteStart = Date.now();
minuteCount = 0;
}
if (++minuteCount > MAX_PER_MINUTE) return;
// ③ 上报(见下)
send(error, context);
}
3) 可靠上报
function send(error: Error, context: ErrorContext) {
const payload = {
name: error.name,
message: error.message,
stack: error.stack,
...context,
breadcrumbs: [...breadcrumbs],
timestamp: Date.now(),
userAgent: navigator.userAgent,
};
const body = JSON.stringify(payload);
// ⭐ 页面关闭时也能可靠发送
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/monitor/error', new Blob([body], { type: 'application/json' }));
return;
}
// 兜底:fetch + keepalive(页面卸载时也能发出)
fetch('/api/monitor/error', {
method: 'POST',
body,
headers: { 'Content-Type': 'application/json' },
keepalive: true,
}).catch(() => { /* 上报失败静默 */ });
}
⭐ 为什么用 sendBeacon / keepalive?
因为错误常发生在「页面即将卸载」时(用户关页面、跳转),
普通的 fetch 会被「中断」→ 错误丢失。
`sendBeacon` 由浏览器「尽力发送」(不阻塞卸载);
`fetch(..., { keepalive: true })` 也能在页面卸载时完成请求(有体积限制,约 64KB)。
4) 源码映射(sourcemap)
问题:生产环境的 stack 是「压缩后的代码位置」
at n.a (https://cdn.example.com/assets/index-4f3a.js:1:23456)
→ 完全无法定位
解法(三种):
① 把 sourcemap 上传到监控平台(Sentry 等支持),由平台「反解」
→ 前端只需要上报「文件名 + 行列号」,平台做映射
② 把 sourcemap 部署到「只有监控服务器能访问」的私有路径
⚠️ 不要把 sourcemap 公开部署(会暴露源码)
③ 开发/测试环境用 development 构建(stack 本身就是可读的)
⚠️ 关键:上报时要带「构建版本 / 文件名哈希」,
否则「不同版本的同名文件」无法正确映射。
五、监控平台的能力(以 Sentry 为例)
// ✅ 平台会帮你做「聚合、去重、sourcemap、性能、会话回放」等
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@o123.ingest.sentry.io/456',
environment: import.meta.env.MODE,
release: __APP_VERSION__, // ⭐ 版本号(用于 sourcemap 与回归判断)
tracesSampleRate: 0.1, // 性能追踪采样率
replaysSessionSampleRate: 0.01,
replaysOnErrorSampleRate: 1.0, // ⭐ 出错时会话回放 100%(最有用)
beforeSend(event) {
// 过滤掉「已知噪音」(如浏览器扩展导致的错误)
if (event.exception?.values?.[0]?.value?.includes('ResizeObserver loop')) return null;
return event;
},
ignoreErrors: [
'ResizeObserver loop limit exceeded', // 常见噪音
'Non-Error promise rejection captured',
/Loading chunk \d+ failed/, // 发版导致的 chunk 404(可用自动刷新处理)
],
});
// 手动上报
Sentry.captureException(error, { extra: { context } });
// 接入 React(自动包 ErrorBoundary 并上报 componentStack)
<Sentry.ErrorBoundary fallback={<ErrorView />}>
<App />
</Sentry.ErrorBoundary>;
⭐ 平台的核心价值(自己搭要写很多):
① 聚合(相同错误归为一条)与「影响用户数」
② sourcemap 自动反解(看到可读堆栈)
③ 「面包屑」与「会话回放」(复现问题)
④ 版本对比(「这个版本新增了哪些错误」)
⑤ 告警(错误量突增 → 通知到群)
⑥ 性能监控(慢接口、慢页面)
⚠️ 隐私与合规:
上报前要「脱敏」——不要把 token、手机号、身份证、完整请求体原样上报。
六、常见坑
// ① 只接了 ErrorBoundary(事件处理/异步/资源的错误全丢)
// ② 只接 window.onerror(拿不到资源加载失败、拿不到 Promise 拒绝)
// ③ 用 onerror 而不是 addEventListener(被覆盖、拿不到资源错误)
window.onerror = ...; // ⚠️ 后面又被别的库覆盖
window.addEventListener('error', fn, true); // ✅
// ④ 资源错误没在「捕获阶段」监听(拿不到事件)
window.addEventListener('error', fn); // ⚠️ 缺少 true
window.addEventListener('error', fn, true); // ✅
// ⑤ 不判断「资源错误 vs JS 错误」(把 <img> 404 当成 JS 崩溃)
// ⑥ 没有去重(循环报错把监控打爆)
// ⑦ 没有限流(错误风暴时上报请求本身变成 DDOS)
// ⑧ 上报用普通 fetch 且不带 keepalive(页面卸载时丢数据)
// ⑨ 没上传 sourcemap(线上堆栈完全不可读)
// ⑩ sourcemap 公开部署(泄露源码)——应该只给监控平台
// ⑪ 上报「未脱敏的敏感数据」(token、手机号、完整请求体)
// ⑫ 忽略了「第三方脚本 / 浏览器扩展」产生的噪音错误
// ⑬ 没有「版本号」(无法判断「是不是这次发版引入的」)
// ⑭ 把「用户主动取消的请求」也上报(AbortError 是噪音)
// ⑮ 在「错误处理器内部」再抛错(递归/死循环)
// ⑯ 没接「接口错误」(接口 4xx/5xx 是最常见的线上问题之一)
// ⑰ 没有「错误量监控与告警」(问题发生几天后才知道)
// ⑱ 在 SSR 中只接客户端监控(服务端错误要单独接)
// ⑭ 的写法:过滤掉「取消请求」与「已处理的错误」
function shouldReport(error: Error, context: ErrorContext) {
if (error.name === 'AbortError') return false; // 主动取消
if (error.message.includes('Loading chunk')) return false; // 由「自动刷新」处理
if (error.message.includes('ResizeObserver loop')) return false; // 浏览器噪音
if (/extension:\/\//.test(error.stack ?? '')) return false; // 浏览器扩展
return true;
}
七、面试延伸
- 「前端错误监控要接哪些?」
四类叠加:① React 渲染错误——用 Error Boundary(componentDidCatch里上报,带componentStack);② 全局 JS 运行时错误——用window.addEventListener('error', fn, true)(捕获阶段,能同时处理「JS 错误」与「资源加载失败」);③ 未处理的 Promise 拒绝——用unhandledrejection事件;④ 接口错误——在「请求层拦截器」里统一上报(带接口路径、方法、状态码)。此外最好用 Sentry 这类平台来获得「聚合、sourcemap 反解、面包屑、会话回放」等能力。 -
「
window.onerror和addEventListener('error')有什么区别?」
四个差异:① 覆盖——window.onerror只能有一个(会被后设置的覆盖),addEventListener可以有多个;② 资源错误——window.onerror拿不到资源加载失败;addEventListener用「捕获阶段(true)」能拿到(此时event.target是那个<img>/<script>);③ 参数——onerror直接给(message, source, lineno, colno, error),addEventListener给一个「ErrorEvent」对象;④ 推荐——用addEventListener('error', fn, true)(更完整、不会被覆盖)。 -
「怎么区分「JS 错误」和「资源加载失败」?」
在error事件里判断「事件目标」:如果event.target是window(或不存在tagName),说明是「JS 运行时错误」(用event.error或event.message);如果event.target是具体的 DOM 元素(有tagName,如IMG/SCRIPT/LINK),说明是「资源加载失败」(可以从target.src/target.href拿到失败的 URL)。注意必须用「捕获阶段」(第三个参数true),否则资源错误事件不会冒泡到window。 -
「为什么要用
sendBeacon?」
因为错误常常发生在「页面即将卸载」的时刻(用户关闭页面、点击跳转)——此时用普通的fetch/XHR上报,请求会被浏览器中断,导致「最需要的那批错误」反而丢失。navigator.sendBeacon(url, data)由浏览器「尽力发送」(不阻塞卸载流程),更适合这种场景;它不可用时可以用fetch(url, { keepalive: true })作为兜底(注意体积限制约 64KB)。 -
「sourcemap 在生产环境怎么处理?」
三个方案:① 上传到监控平台(Sentry 等支持sourcemaps上传),平台在展示时自动「反解」堆栈——前端只需上报文件名与行列号,这是主流做法;② 把 sourcemap 部署到「只有监控服务器能访问」的私有地址(前端不上传,由服务端拉取反解);③ 开发/测试环境用 development 构建(堆栈本身可读)。核心原则:不要公开部署 sourcemap(会暴露源码与注释),但一定要保留它用于排查;同时上报时必须带构建版本/文件名哈希,否则多版本同名文件无法正确映射。 -
「上报错误要注意什么?」
五件事:① 上下文——带上「路由、用户 ID、应用版本、UA、componentStack、面包屑(最近的操作序列)」,这对复现至关重要;② 去重——用「错误名 + 消息 + 栈首行」做指纹,短时间内相同错误只报一次;③ 限流——限制「每分钟上报条数」(防止「循环报错」把监控打爆,甚至把上报接口变成 DDoS 源);④ 可靠上报——用sendBeacon/keepalive(页面卸载也能发出);⑤ 脱敏——不要把 token、手机号、身份证、完整请求体原样上报(合规风险)。 -
「哪些错误应该被过滤掉?」
四类噪音:① 主动取消的请求(AbortError/CanceledError)——这是我们自己的行为;② 发版导致的 chunk 加载失败(Loading chunk N failed)——通常由「自动刷新」处理(见 6.8);③ 浏览器/规范已知的噪音——如ResizeObserver loop limit exceeded(Chrome 的常见告警,实际无害);④ 浏览器扩展注入的错误——堆栈里出现extension://、chrome-extension://或明确来自扩展脚本的错误。不过滤会造成「告警疲劳」,导致真正的问题被淹没。 -
「怎么让上报的错误『能定位到代码』?」
三个关键:① sourcemap 反解(见上)——这是「从压缩堆栈到源码」的唯一途径;②componentStack——React 错误特有的「组件链」信息,能定位到「哪个组件」而不是「哪一行 JS」;③ 面包屑 + 会话回放——记录「错误发生前用户做了什么」(点击、路由、接口),这对「无法稳定复现」的问题至关重要。此外要带「版本号」,否则无法判断「是不是这次发版引入的」。
一句话速记
错误监控要「四者叠加」:ErrorBoundary(渲染错误,上报
componentStack) +addEventListener('error', fn, true)(全局 JS 错误 + 资源加载失败,靠event.target区分) +unhandledrejection(未处理 Promise) + 请求层拦截器(接口错误);上报要做到「带上下文(路由/用户/版本/面包屑)、去重(指纹)、限流(防风暴)、sendBeacon/keepalive可靠发送、脱敏」;sourcemap 一定要留给监控平台反解(但别公开部署),并带上版本号;过滤噪音(AbortError、chunk 加载失败、ResizeObserver loop、扩展注入)避免告警疲劳;生产实践直接用 Sentry(聚合 + sourcemap + 面包屑 + 会话回放 + 告警)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!