没有 Error Boundary 的 React 应用:
<App>
<Header />
<UserProfile /> {/* throw Error → 整棵树白屏 */}
<Footer />
</App>
一个组件出错,整棵树卸载。用户看到白屏。
有 Error Boundary 的 React 应用:
<App>
<Header />
<ErrorBoundary fallback={<Error />}>
<UserProfile /> {/* throw Error → 只有这部分降级 */}
</ErrorBoundary>
<Footer />
</App>
一个组件出错,只有被 Error Boundary 包裹的部分降级。其他部分正常工作。
Error Boundary 是 React 处理"渲染失败"的唯一机制。
一、为什么需要 Error Boundary
1.1 UI 是一棵树
React 的 UI 是一棵组件树。树的特点是:一个节点的变化会影响整棵子树。
<App>
<Layout>
<Sidebar />
<Content>
<UserProfile /> {/* 出错 */}
</Content>
</Layout>
</App>
UserProfile 出错时,它不能"自己处理自己"——因为它已经 throw 了,函数执行中断了。
需要一个机制在 throw 之前"接住"这个错误。
1.2 try/catch 不够
为什么不用 try/catch?
function App() {
try {
return <UserProfile />
} catch (error) {
return <Error />
}
}
问题:render 阶段可以被中断、恢复、丢弃。
React 的 render 不是"调用一次函数"。它可能:
- 调用组件函数
- 中断(有更高优先级的任务)
- 重新调用(从头开始)
- 再中断
- 最终完成
如果在 try/catch 里,中断时 catch 了什么?重新调用时 try 了什么?语义完全混乱。
所以 React 需要自己的错误边界机制,和 render 生命周期绑定。
二、Error Boundary 的实现
2.1 Class Component 的生命周期
Error Boundary 只能用 Class Component 实现(截至目前):
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null }
static getDerivedStateFromError(error) {
// 更新 state,下次渲染显示 fallback
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// 上报错误到监控系统
reportError(error, errorInfo.componentStack)
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
两个生命周期:
- getDerivedStateFromError:纯函数,根据错误更新 state
- componentDidCatch:副作用,上报错误
2.2 为什么没有 hooks 版本?
因为 Error Boundary 需要在 render 阶段同步捕获错误。hooks 的 useEffect 是异步的(paint 后执行),来不及。
// 不可能的 hooks 版本
function ErrorBoundary({ children, fallback }) {
const [error, setError] = useState(null)
// ❌ 这不行——render 阶段 throw 了,useEffect 根本不会执行
useEffect(() => {
// 永远到不了这里
}, [])
if (error) return fallback
return children
}
React 团队考虑过添加 useErrorBoundary hook,但设计上一直有争议。目前只能用 Class Component。
三、错误的传播路径
3.1 throw → 向上冒泡
<App>
<ErrorBoundaryA>
<Layout>
<ErrorBoundaryB>
<UserProfile /> {/* throw Error */}
</ErrorBoundaryB>
</Layout>
</ErrorBoundaryA>
</App>
UserProfile throw Error:
- 检查 ErrorBoundaryB → 有 componentDidCatch → 捕获
- ErrorBoundaryB 显示 fallback
- Layout、App 正常渲染
如果 ErrorBoundaryB 不存在:
- 向上冒泡到 Layout → 不是 Error Boundary,继续冒泡
- 到 ErrorBoundaryA → 捕获
- ErrorBoundaryA 显示 fallback
- App 正常渲染
错误向上冒泡,直到被最近的 Error Boundary 捕获。
3.2 Error Boundary 也 throw 了怎么办?
<ErrorBoundaryA fallback={<ErrorA />}>
<ErrorBoundaryB fallback={<ErrorB />}>
<UserProfile /> {/* throw Error */}
</ErrorBoundaryB>
</ErrorBoundaryA>
UserProfile throw → ErrorBoundaryB 捕获 → 渲染 fallback
但如果 ErrorBoundaryB 的 fallback 也 throw 了:
<ErrorBoundaryA fallback={<ErrorA />}>
<ErrorBoundaryB fallback={<BrokenFallback />}>
{' '}
{/* fallback 也 throw */}
<UserProfile /> {/* throw Error */}
</ErrorBoundaryB>
</ErrorBoundaryA>
- UserProfile throw → ErrorBoundaryB 损获
- 渲染 BrokenFallback → BrokenFallback 也 throw
- ErrorBoundaryB 自己也挂了 → 向上冒泡
- ErrorBoundaryA 捕获 → 渲染 ErrorA
Error Boundary 自己出错时,错误继续向上冒泡。
四、哪些错误能捕获,哪些不能
4.1 能捕获的
// render 阶段的错误
function UserProfile() {
throw new Error('render failed')
}
// 生命周期的错误
class UserProfile extends React.Component {
componentDidMount() {
throw new Error('mount failed')
}
render() {
return <div>{this.props.user.name}</div> // 如果 user 是 undefined,throw
}
}
// 子组件的错误
function App() {
return (
<ErrorBoundary>
<UserProfile /> {/* 内部 throw */}
</ErrorBoundary>
)
}
4.2 不能捕获的
// ❌ 事件处理器
function Button() {
const handleClick = () => {
throw new Error('click failed') // Error Boundary 捕获不到
}
return <button onClick={handleClick}>Click</button>
}
// ✅ 正确做法:事件处理器自己 try/catch
function Button() {
const handleClick = () => {
try {
doSomething()
} catch (error) {
// 自己处理,或者调用 setState 触发 Error Boundary
setError(error)
}
}
return <button onClick={handleClick}>Click</button>
}
// ❌ 异步代码
async function fetchUser() {
throw new Error('fetch failed') // Error Boundary 捕获不到
}
// ❌ useEffect
useEffect(() => {
throw new Error('effect failed') // Error Boundary 捕获不到
}, [])
规则:Error Boundary 只能捕获同步的、render 阶段的错误。
为什么?因为事件处理器、异步代码、useEffect 都在 render 之外执行。React 没有机制在这些地方"插入"错误边界。
五、Error Boundary 的 Ownership 模型
回到 Law 3 — Fiber Ownership。
Error Boundary 也是一种 Ownership:
ErrorBoundary
├── 拥有:子树的渲染生命周期
├── 负责:捕获子树的渲染错误
└── 决定:出错时显示什么
5.1 边界划分
<Layout>
<Header />
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Content />
</ErrorBoundary>
<Footer />
</Layout>
三个独立的边界:
- Header 出错 → 无边界 → 整棵树白屏
- Sidebar 出错 → SidebarError
- Content 出错 → ContentError
- Footer 出错 → 无边界 → 整棵树白屏
Error Boundary 的位置决定了错误的影响范围。
5.2 粒度选择
// 粗粒度:整个页面一个边界
<ErrorBoundary fallback={<PageError />}>
<App />
</ErrorBoundary>
// 细粒度:每个组件一个边界
<ErrorBoundary fallback={<HeaderError />}>
<Header />
</ErrorBoundary>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Content />
</ErrorBoundary>
粒度越细:
- 优点:局部失败不影响全局,用户体验好
- 缺点:代码冗余,边界太多维护成本高
实际项目中,通常在布局层级划分边界:
<ErrorBoundary fallback={<AppError />}>
<Layout>
<Header />
<ErrorBoundary fallback={<MainError />}>
<MainContent />
</ErrorBoundary>
<Footer />
</Layout>
</ErrorBoundary>
六、Error Boundary 与 Suspense 的关系
两个边界,各管各的:
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
| 场景 | 结果 |
|---|---|
| UserProfile 正常渲染 | 显示 UserProfile |
| UserProfile suspend (throw thenable) | 显示 Loading(Suspense 捕获) |
| UserProfile throw Error | 显示 Error(ErrorBoundary 捕获) |
| Loading 组件 throw Error | 显示 Error(穿透 Suspense,ErrorBoundary 捕获) |
6.1 双重边界
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
UserProfile throw thenable → Suspense 捕获 → 显示 Loading
UserProfile throw Error → 穿透 Suspense → ErrorBoundary 捕获 → 显示 Error
Loading 组件 throw Error → 穿透 Suspense → ErrorBoundary 捕获 → 显示 Error
Suspense 只捕获 thenable,Error 只能被 ErrorBoundary 捕获。
七、最佳实践
7.1 在布局层级划分边界
function App() {
return (
<ErrorBoundary fallback={<GlobalError />}>
<Layout>
<Header />
<ErrorBoundary fallback={<MainError />}>
<MainContent />
</ErrorBoundary>
<Footer />
</Layout>
</ErrorBoundary>
)
}
- 全局边界:兜底,防止整棵树白屏
- 局部边界:保护主要内容区域
7.2 提供有意义的 fallback
// ❌ 不好的 fallback
<ErrorBoundary fallback={<div>出错了</div>}>
<UserProfile />
</ErrorBoundary>
// ✅ 好的 fallback
<ErrorBoundary fallback={({ error, resetError }) => (
<div>
<p>加载用户信息时出错:{error.message}</p>
<button onClick={resetError}>重试</button>
</div>
)}>
<UserProfile />
</ErrorBoundary>
提供重试机制。用户可以点击"重试"重新渲染子树。
7.3 错误上报
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// 上报到监控系统
reportError({
error,
componentStack: errorInfo.componentStack,
// 可以附加额外信息
userId: this.props.userId,
page: window.location.pathname,
})
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
componentDidCatch 是副作用,适合做错误上报。
7.4 配合路由
function Router() {
return (
<ErrorBoundary fallback={<RouteError />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</ErrorBoundary>
)
}
路由级别的 Error Boundary:一个页面出错,不影响其他页面。
八、与五个 Law 的关系
| Law | Error Boundary 的角色 |
|---|---|
| Law 1 (Environment) | Error Boundary 不区分 Server/Client,两端都能用 |
| Law 2 (Description) | 当 Description 失败时,Error Boundary 决定 Reality 显示什么 |
| Law 3 (Ownership) | Error Boundary 拥有子树的渲染生命周期,负责捕获渲染错误 |
| Law 4 (Lane) | Error Boundary 捕获的错误是同步的,不涉及优先级调度 |
| Law 5 (Cost) | Error Boundary 避免整棵树重渲染(局部降级 vs 全局白屏) |
九、总结
Error Boundary 的核心设计:
- 它是 Ownership 的体现:谁拥有这个子树,谁负责处理它的渲染错误
- 它是同步的:只能捕获 render 阶段的错误,事件处理器和异步代码自己处理
- 它是冒泡的:错误向上冒泡,直到被最近的 Error Boundary 捕获
- 它是可组合的:可以嵌套,内层边界优先捕获
最终目标:
让局部失败不影响全局,让用户尽可能看到有意义的内容。
这就是 Error Boundary 的 Ownership 哲学。