起因
一个生产 bug:某个组件渲染时 throw 了未捕获错误 → 整页白屏 →
用户刷新无效 → 截图发给客服。
React 默认行为:子组件 throw 没 catch → 整个组件树卸载 → 白屏。
正确做法:用 ErrorBoundary 局部捕获 + 显示"出错了"卡片 + 上报错误到
Sentry。
解决方案
1. 基础 ErrorBoundary
// components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, info: { componentStack: string }) => void
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: { componentStack: string }) {
console.error('ErrorBoundary caught:', error, info)
this.props.onError?.(error, info)
}
reset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return (
<div role="alert" style={{ padding: 16, background: '#fee', borderRadius: 8 }}>
<h3>出错了 😞</h3>
<p>{this.state.error?.message}</p>
<button onClick={this.reset}>重试</button>
</div>
)
}
return this.props.children
}
}
ErrorBoundary 必须 class component(Hooks 不支持 componentDidCatch)。
2. 用法:包裹"应该独立失败"的区域
function Dashboard() {
return (
<Layout>
<ErrorBoundary>
<PostList /> {/* 出错只这里降级 */}
</ErrorBoundary>
<ErrorBoundary>
<RecommendedSidebar />
</ErrorBoundary>
<ErrorBoundary>
<CommentFeed />
</ErrorBoundary>
</Layout>
)
}
任意一个组件挂了不影响其它,用户至少能用其它部分。
3. 顶层兜底
function App() {
return (
<ErrorBoundary
fallback={
<div style={{ padding: 40, textAlign: 'center' }}>
<h1>应用出错</h1>
<p>请刷新页面,或联系支持</p>
<button onClick={() => location.reload()}>刷新</button>
</div>
}
>
<Router>
<Routes>...</Routes>
</Router>
</ErrorBoundary>
)
}
4. Sentry 集成(生产必备)
npm i @sentry/react
// main.tsx
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: 'https://[email protected]/123',
environment: import.meta.env.MODE,
tracesSampleRate: 0.1, // 10% 性能采样
replaysSessionSampleRate: 0.1, // 10% 会话录制
replaysOnErrorSampleRate: 1.0, // 出错的 100% 录制
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: false, blockAllMedia: true }),
],
})
用 Sentry 的 ErrorBoundary 替代自己写的:
import * as Sentry from '@sentry/react'
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => (
<ErrorCard error={error} onReset={resetError} />
)}
showDialog // 弹用户反馈表单
>
<PostList />
</Sentry.ErrorBoundary>
任何 error throw → 自动上报 Sentry → 包含 stack trace + 浏览器版本 +
URL + 用户 ID + session replay 视频回放。
5. 异步错误:try/catch + Sentry.captureException
ErrorBoundary 只抓 render / lifecycle 错误。异步错误(fetch /
setTimeout / Promise)抓不到:
async function loadData() {
try {
const r = await fetch('/api/users')
if (!r.ok) throw new Error(`status ${r.status}`)
return await r.json()
} catch (e) {
Sentry.captureException(e)
throw e // 让上层 React Query / useState 知道失败
}
}
或者 React Query 自动:
const { data, error } = useQuery({
queryKey: ['users'],
queryFn: loadData,
throwOnError: true, // throw 给最近的 ErrorBoundary
})
throwOnError: true 让 TanStack Query 把 error 抛给 ErrorBoundary,
两套机制串起来。
6. 区分预期错误 vs 真 bug
// 预期错误:业务规则
async function transfer(amount) {
const r = await fetch('/api/transfer', ...)
if (r.status === 403) {
throw new InsufficientBalanceError() // 不上报 Sentry
}
if (!r.ok) {
throw new Error('transfer failed') // 上报
}
}
class InsufficientBalanceError extends Error {
isExpected = true
}
// 包装
function captureIfUnexpected(e: Error) {
if ((e as any).isExpected) return
Sentry.captureException(e)
}
Sentry 项目里大量"用户输错密码 401"的告警没价值,要区分。
7. 全局未捕获错误
window.addEventListener('error', (event) => {
Sentry.captureException(event.error)
})
window.addEventListener('unhandledrejection', (event) => {
Sentry.captureException(event.reason)
})
Sentry SDK 默认已经 hook 了这些,不需要自己写。
8. Source maps 上传
生产 bundle 是 minified;Sentry 看 stack trace 全是 a.b.c at xyz.js:1:1234,
没法定位。Vite 配 sourcemap + 上传到 Sentry:
// vite.config.ts
import { sentryVitePlugin } from '@sentry/vite-plugin'
export default defineConfig({
build: { sourcemap: true },
plugins: [
react(),
sentryVitePlugin({
org: 'my-org',
project: 'my-app',
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
})
build 时自动上传 sourcemap 到 Sentry,error 在 dashboard 显示原始
TS 代码 + 行号。
9. 用户反馈
<Sentry.ErrorBoundary
fallback={({ error, eventId }) => (
<div>
<h3>出错了</h3>
<p>已自动上报。错误 ID: <code>{eventId}</code></p>
<button onClick={() => Sentry.showReportDialog({ eventId })}>
提供反馈
</button>
</div>
)}
>
用户能描述"我点什么按钮触发的",Sentry 把这个反馈跟 error 绑在一起。
效果
- 白屏 bug 数从每月 10+ → 0(局部降级)
- Sentry 收集到 95% bug,开发能主动修而不是等用户报
- session replay 让"为什么会出现这个 state" 一目了然
- 半年内 frontend error rate 从 2% → 0.3%
注意事项
1. 不要包太细
每个 div 一个 ErrorBoundary:渲染开销 + 视觉细碎。
原则是"独立可用的功能块"为单位(侧栏 / 主内容 / 评论区)。
2. PII 信息
Sentry 默认抓表单数据。可能包含密码、邮箱、信用卡。
Sentry.init({
beforeSend(event) {
// 删 password 字段
if (event.request?.data) {
delete event.request.data.password
}
return event
},
})
或者 mask all replay:replayIntegration({ maskAllText: true })。
3. quota
Sentry 免费层每月 5k events。tracesSampleRate: 0.1 限性能采样。
真 error 全部上报;性能 / replay 采样。
踩过的坑
-
ErrorBoundary 必须是 class:function component 用 React 19+
的use()可以一定程度替代,但官方 ErrorBoundary 仍是 class。 -
getDerivedStateFromError不能调 setState:用 return 新 state。
componentDidCatch才能调副作用 / 上报。 -
dev 模式 React 显示完整错误覆盖层:不代表 ErrorBoundary 没工作。
生产 build 才看到 fallback。 -
forgotten reset:fallback 里写 "重试" 按钮要调
resetError(),
而不是setState({ hasError: false })(state 在 boundary 里,
按钮在 fallback 里)。Sentry boundary 的resetErrorprop 帮你
处理。 -
SSR 时 ErrorBoundary:getDerivedStateFromError 在 SSR 跑 →
服务端如果 catch 到错误就 render fallback HTML。客户端 hydrate
时如果不同会 mismatch warning。
登录后参与评论。