Error Boundary:兜底、重试与上报(自研 vs Sentry)(机制/架构视角)
一句话结论:Error Boundary 负责处理 React 渲染链路上的“同步异常”(render/生命周期/构造函数等)——当子树抛错时,React 会在最近的 Error Boundary 处中止该子树的提交并渲染 fallback;真正的工程落地关键在于:边界如何分层、如何 reset/retry、以及错误上报体系(自研或 Sentry)如何做到版本关联、去重、采样、隐私与可观测闭环。
1. Error Boundary 能捕获什么?不能捕获什么?(必须先讲清)
1.1 能捕获(React 渲染链路的同步错误)
通常包括:
- render 阶段抛错(函数组件执行、class render)
- 生命周期抛错(class 组件的生命周期)
- 子组件构造函数抛错(class 组件)
当这些错误发生时,React 会沿 Fiber 向上寻找最近的 Error Boundary。
1.2 不能捕获(最常见误区)
Error Boundary 捕获不到:
- 事件回调里的错误(onClick/onChange 内 throw)
- 异步回调里的错误(setTimeout / promise 链 / async/await 中 throw)
- 自定义事件/订阅回调里的错误(WebSocket message、RxJS subscribe 等)
- SSR 服务端执行时抛出的错误(这属于服务端错误处理体系)
这些错误需要靠:
- try/catch(业务层)
window.onerror/unhandledrejection(全局兜底)- 统一请求层/订阅层的错误处理
- 服务端日志与 tracing
2. 机制:React 在出错时做了什么?
把它放到你已有的 Fiber 认知里更好理解:
- render 阶段:构建 workInProgress 树,可能会执行到某个组件并抛错
- React 捕获到错误后,会尝试找到最近的 Error Boundary(本质上是一个带特定能力的祖先 Fiber)
- 一旦找到边界:
- React 会把该边界标记为需要处理错误(可理解为“在这个边界处降级”)
- 丢弃出错子树的本次渲染结果
- 在 commit 时,渲染边界的 fallback UI(而不是把错误子树提交到 DOM)
这个过程体现了 React 的核心边界:
- render 可以失败、可以丢弃
- commit 要么成功提交一棵一致的 UI,要么回退到 fallback
3. Error Boundary 与 Suspense:一个处理“错”,一个处理“等”
- Suspense 处理的是 pending(等待):suspend → fallback(loading)
- Error Boundary 处理的是 error(异常):throw error → fallback(error UI)
架构上建议把两者分治:
- 同一区域既可能“等”也可能“错”时,通常是:
- 外层
ErrorBoundary - 内层
Suspense
- 外层
这样:
- 加载失败不会被 loading UI 吞掉
- 错误 UI 不会和 loading 状态混在一起
4. “边界怎么切”:错误兜底的分层策略(P0)
4.1 三层边界(推荐)
- App-level(全局):兜底最后一道防线(避免白屏)
- Route-level(页面级):页面可降级但壳不崩(导航/菜单稳定)
- Section-level(模块级):列表/详情/评论等局部失败不影响整体
4.2 反例
- 只有一个全局边界:任何小错误都导致整页错误页(体验差)
- 过度碎片化:上报量暴增、fallback 频繁闪烁、用户迷惑
5. Recovery:如何“重试/恢复”(resetKeys / onReset)
一个好的错误兜底不只是展示错误信息,更关键是能恢复:
- 用户点“重试”
- 或者路由变化/参数变化时自动 reset
5.1 使用 react-error-boundary(推荐的工程化方式)
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div>
<p>模块加载失败:{String(error?.message ?? error)}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)
}
export function Section() {
const queryKey = 'users:list'
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[queryKey]}
onReset={() => {
// 清缓存/重拉数据等
}}
onError={(error, info) => {
// 上报(见下文)
}}
>
<UsersList />
</ErrorBoundary>
)
}
要点:
resetKeys让边界在关键输入变化时自动恢复onReset做“恢复动作”(清缓存、invalidate query、reset store)
6. 上报体系:自研 vs Sentry(适配不同企业策略)
6.1 共同目标(无论自研还是 Sentry 都要做到)
- 版本关联:release / commit / buildId
- 去重聚合:按 stack + message + route 聚合
- 采样与限流:避免错误风暴打爆后端
- 上下文:用户/租户/路由/feature flag/网络状态
- SourceMap:线上堆栈可还原(注意安全策略)
- 隐私合规:PII 脱敏、字段白名单
6.2 方案 A:接入 Sentry(开箱即用,适合效率优先)
优点:
- 聚合、告警、趋势、Release、SourceMap、性能/Tracing 能力成熟
- 生态完善,React 集成很顺
关键落地点:
- 初始化与 release
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.APP_RELEASE, // commit hash 或版本号
environment: process.env.APP_ENV,
tracesSampleRate: 0.0, // 需要再开
})
- 在 Error Boundary 的
onError中 capture
onError={(error, info) => {
Sentry.withScope(scope => {
scope.setTag('boundary', 'users-section')
scope.setContext('react', { componentStack: info?.componentStack })
Sentry.captureException(error)
})
}}
- 事件回调/异步错误
- Sentry 通常也能通过全局 hook 捕获
window.onerror / unhandledrejection,但你仍建议在关键异步链路里显式捕获并补充上下文。
适配建议:
- 大多数团队采用:Sentry 做主链路 + 自研做补充(业务字段/审计)。
6.3 方案 B:自研上报(可控、适合合规/私有化/成本敏感)
自研的核心不是“发一个请求”,而是要做“可运营的错误平台”的最小闭环。
6.3.1 事件模型(建议)
eventIdtimestamprelease(版本)route/pathname/searcherrorName/messagestackcomponentStack(Error Boundary 能提供)userId/tenantId(可选,注意脱敏)device/ua/networktags(feature / boundary / module)
6.3.2 客户端 SDK(最小实现)
type ReportPayload = {
release: string
message: string
stack?: string
componentStack?: string
route?: string
tags?: Record<string, string>
}
export async function reportError(payload: ReportPayload) {
// 去重/采样(示意)
const sampled = Math.random() < 0.1
if (!sampled) return
navigator.sendBeacon?.('/api/fe-error', JSON.stringify(payload))
?? fetch('/api/fe-error', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true,
})
}
6.3.3 在 Error Boundary 中上报
onError={(error, info) => {
reportError({
release: __APP_RELEASE__,
message: String(error?.message ?? error),
stack: error?.stack,
componentStack: info?.componentStack,
route: location.pathname + location.search,
tags: { boundary: 'users-section' },
})
}}
6.3.4 服务端接收(全栈视角建议)
- 做限流(IP/租户/版本维度)
- 做聚合键(stack hash)
- 存储:ClickHouse/ES/PG 都行(看规模)
- 告警:按“版本新增错误”“错误率突增”“关键路径错误”
自研策略更适合:私有化部署、强合规(不出网)、或希望把错误与业务指标/审计打通的企业。
6.4 混合策略:同一套边界,双通道可切换
很多企业会要求:
- 国内/私有化:走自研
- 海外/效率优先:走 Sentry
建议做成“上报适配层”:
export interface ErrorReporter {
capture(error: unknown, ctx: Record<string, any>): void
}
export const reporter: ErrorReporter = isSentryEnabled
? sentryReporter
: selfHostedReporter
业务侧只依赖 reporter.capture,避免 vendor lock-in 扩散。
7. Next.js 与 TanStack Router 的落地点
7.1 Next.js App Router
- route segment 可以用
error.tsx做段级错误页 - 仍建议在客户端交互密集模块内使用组件级 Error Boundary(更细粒度、更可恢复)
- 上报可在
error.tsx内或通过统一 client SDK 完成
7.2 TanStack Router + Vite
- 路由层可在 route component 外包一层 route-level Error Boundary
- 模块层用 section-level Error Boundary
- 配合 TanStack Query:错误页的“重试”通常对应
queryClient.invalidateQueries(...)
8. Checklist
- 我是否清楚哪些错误 Error Boundary 捕获不到?(事件/异步/订阅)
- 边界是否分层(app/route/section),壳是否稳定?
- fallback 是否提供恢复路径(重试/返回/刷新),并且 resetKeys 合理?
- 上报是否做了 release/version 关联与 SourceMap 策略?
- 上报是否做了采样/限流/去重,避免错误风暴?
- 是否有隐私脱敏策略(PII)?
关联阅读
- Suspense(处理“等”):
notes/09-react-18-in-practice/04-suspense-loading-boundary-design.md - StrictMode(暴露副作用问题):
notes/09-react-18-in-practice/02-strictmode-and-idempotent-effects.md - 事件系统(事件回调错误不进边界):
notes/07-event-system/01-overview.md