React Error Boundary + Sentry:把"白屏" 救成"局部降级 + 自动上报"

起因

一个生产 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 采样。

踩过的坑

  1. ErrorBoundary 必须是 class:function component 用 React 19+
    use() 可以一定程度替代,但官方 ErrorBoundary 仍是 class。

  2. getDerivedStateFromError 不能调 setState:用 return 新 state。
    componentDidCatch 才能调副作用 / 上报。

  3. dev 模式 React 显示完整错误覆盖层:不代表 ErrorBoundary 没工作。
    生产 build 才看到 fallback。

  4. forgotten reset:fallback 里写 "重试" 按钮要调 resetError()
    而不是 setState({ hasError: false })(state 在 boundary 里,
    按钮在 fallback 里)。Sentry boundary 的 resetError prop 帮你
    处理。

  5. SSR 时 ErrorBoundary:getDerivedStateFromError 在 SSR 跑 →
    服务端如果 catch 到错误就 render fallback HTML。客户端 hydrate
    时如果不同会 mismatch warning。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。