Sentry 异常捕获原理:onerror、unhandledrejection 与 React Error Boundary 的整合
各位开发者朋友,大家好!今天我们来深入探讨一个在现代前端开发中非常关键的话题——异常捕获机制。尤其当我们使用像 Sentry 这样的监控工具时,理解底层原理不仅有助于我们更高效地调试问题,还能帮助我们在架构层面做出更合理的决策。
本文将围绕三个核心知识点展开:
- 全局错误监听:
window.onerror和window.addEventListener('unhandledrejection', ...) - React 中的 Error Boundary(错误边界)
- 如何将 Sentry 与上述两种机制无缝整合
我们将从底层原理讲起,逐步过渡到实际代码示例,并最终给出一套完整的整合方案。全程不堆砌术语,只用清晰逻辑和真实可运行的代码来说明问题。
一、为什么需要异常捕获?
在浏览器环境中,JavaScript 是单线程执行的,一旦某个地方抛出未处理的异常(比如语法错误、网络请求失败、Promise 拒绝等),整个页面可能会崩溃或进入不可预测状态。对于用户来说,这可能是“白屏”、“按钮失效”甚至“数据丢失”。
因此,我们需要一套系统化的异常捕获策略:
- 捕获 JavaScript 运行时错误(如语法错误、类型错误)
- 捕获 Promise 拒绝事件(如 API 请求失败未被 catch)
- 捕获 React 组件渲染过程中的错误(通过 Error Boundary)
这些就是我们今天要重点分析的内容。
二、全局错误监听机制:onerror 和 unhandledrejection
2.1 window.onerror —— 全局 JS 错误捕获
这是最古老的全局错误捕获方式之一。当任何未被捕获的 JavaScript 错误发生时,会触发这个事件处理器。
window.onerror = function(message, source, lineno, colno, error) {
console.log('Global JS Error:', message);
console.log('Source:', source);
console.log('Line:', lineno);
console.log('Column:', colno);
// 发送到 Sentry
Sentry.captureException(error);
return true; // 阻止默认行为(防止浏览器弹窗)
};
✅ 优点:兼容性好,几乎所有浏览器都支持
❗ 注意事项:
- 只能捕获同步错误(非异步)
- 不包含 Promise 抛出的错误(见下文)
2.2 window.addEventListener('unhandledrejection') —— Promise 拒绝监听
这是 ES6 引入的新特性,专门用于监听那些没有被 .catch() 处理的 Promise 拒绝事件。
window.addEventListener('unhandledrejection', function(event) {
const reason = event.reason;
console.log('Unhandled Rejection:', reason);
// 发送到 Sentry
Sentry.captureException(reason);
// 必须调用 preventDefault() 否则浏览器可能报错
event.preventDefault();
});
✅ 优点:可以捕获异步错误(如 fetch 失败、setTimeout 内部错误)
❗ 注意事项:
- 如果你用了
Promise.all或Promise.race等组合方法,也要小心它们内部的错误传播- 不会自动阻止浏览器控制台打印错误信息(除非调用
preventDefault())
📌 重要对比表:
| 特性 | onerror |
unhandledrejection |
|---|---|---|
| 是否捕获同步错误 | ✅ 是 | ❌ 否 |
| 是否捕获异步错误(Promise) | ❌ 否 | ✅ 是 |
| 浏览器兼容性 | ⭐️ 极佳(IE8+) | ⭐️ 较好(Chrome 49+, Firefox 45+) |
| 是否需手动阻止默认行为 | ✅ 是(返回 true) | ✅ 是(调用 preventDefault()) |
这两个机制构成了我们前端应用中最基础的“兜底”异常收集层。
三、React Error Boundary —— 组件级异常隔离
虽然上面两个机制可以捕获全局错误,但它们无法感知 React 组件内部的具体错误来源。比如某个组件在 render 或生命周期函数中抛错,会导致整个 App 崩溃。
React 提供了 Error Boundary 来解决这个问题。它是一个特殊的 React 组件,用来捕获子组件树中的 JavaScript 错误,并展示降级 UI。
3.1 如何实现一个 Error Boundary?
import React, { Component } from 'react';
import * as Sentry from '@sentry/react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 将错误发送到 Sentry
Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
},
});
console.error('Error caught by boundary:', error);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', backgroundColor: '#fdd' }}>
<h2>Something went wrong.</h2>
<p>Please try again later.</p>
</div>
);
}
return this.props.children;
}
}
✅ 优点:
- 能精确捕获组件级别的错误(包括 render、componentDidMount、componentDidUpdate)
- 支持自定义降级 UI,提升用户体验
- 可以附加额外上下文(如
componentStack)传给 Sentry,方便定位
❗ 注意事项:
- Error Boundary 只能包裹 Class Component(因为基于
getDerivedStateFromError和componentDidCatch生命周期)- 函数式组件 + Hooks 不直接支持,但可通过封装成高阶组件(HOC)或自定义 Hook 实现(稍后介绍)
四、整合方案:Sentry + 全局监听 + Error Boundary
现在我们把前面三种机制整合起来,形成一个完整的前端异常监控体系。
4.1 完整配置示例(React + Sentry)
// src/sentrySetup.js
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
integrations: [
new Integrations.BrowserTracing(),
],
tracesSampleRate: 1.0,
});
// 全局错误捕获
window.onerror = function(message, source, lineno, colno, error) {
Sentry.captureException(error);
return true;
};
// Promise 拒绝捕获
window.addEventListener('unhandledrejection', function(event) {
Sentry.captureException(event.reason);
event.preventDefault();
});
// React Error Boundary(作为根组件包裹)
const AppWithBoundary = () => (
<ErrorBoundary>
<App />
</ErrorBoundary>
);
这样做的好处是:
| 层级 | 捕获能力 | Sentry 行为 |
|---|---|---|
全局 JS 错误 (onerror) |
所有未捕获的同步错误 | captureException |
Promise 错误 (unhandledrejection) |
所有未捕获的异步错误 | captureException |
| React 组件错误 | 渲染/生命周期错误 | captureException 并附带 componentStack |
✅ 这样我们就实现了三层防护:从全局到局部,从同步到异步,确保不会有任何异常被遗漏!
五、进阶技巧:让 Error Boundary 更强大
5.1 自定义 Hook 实现函数式组件的 Error Boundary
如果你主要使用函数式组件,可以创建一个通用的 useErrorBoundary Hook:
import { useState, useCallback } from 'react';
import * as Sentry from '@sentry/react';
export function useErrorBoundary() {
const [hasError, setHasError] = useState(false);
const handleError = useCallback((error, info) => {
Sentry.captureException(error, {
extra: {
componentStack: info.componentStack,
},
});
setHasError(true);
}, []);
return { hasError, handleError };
}
// 使用示例
function MyComponent() {
const { hasError, handleError } = useErrorBoundary();
if (hasError) {
return <div>Something went wrong!</div>;
}
try {
// 可能出错的逻辑
throw new Error('This is a test error');
} catch (err) {
handleError(err, { componentStack: 'MyComponent' });
}
return <div>Hello World</div>;
}
✅ 这种方式适合不想改写现有代码结构的团队,也能灵活嵌入任意函数组件中。
六、常见陷阱与最佳实践
| 问题 | 解决方案 |
|---|---|
| Sentry 未捕获某些错误? | 确保已启用 onerror 和 unhandledrejection 监听,并检查是否漏掉某些异步场景(如 setTimeout、fetch) |
| Error Boundary 不生效? | 确认包裹的是 Class Component;如果是函数组件,请使用 HOC 或自定义 Hook |
| 重复上报同一错误? | Sentry 默认会去重,但如果手动调用 captureException 多次,建议加日志标记或防抖机制 |
| 性能影响? | 监听器本身开销极小,但频繁上报可能影响性能,建议设置 tracesSampleRate 控制采样率 |
七、总结:构建健壮的前端异常监控体系
今天我们系统性地梳理了:
window.onerror和unhandledrejection是前端异常捕获的基石,覆盖绝大多数运行时错误;- React Error Boundary 提供了细粒度的组件级错误隔离,极大提升用户体验;
- Sentry 作为主流监控平台,能够将这些错误结构化上报并提供丰富的上下文(如组件栈、用户行为等);
- 最终整合方案让我们的应用具备“三层防护”能力:全局兜底 → 异步兜底 → 组件级兜底。
💡 建议你在项目初期就引入这套机制,而不是等到线上出了问题再去补救。记住一句话:
“好的错误处理不是为了掩盖 bug,而是为了让用户感受到稳定,让开发者快速定位问题。”
希望这篇文章对你有所帮助。如果你正在搭建或优化前端监控系统,不妨试试这套组合拳吧!
📌 文末附赠:推荐阅读材料
祝大家写出更稳定的前端代码!