没有 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:

  1. 检查 ErrorBoundaryB → 有 componentDidCatch → 捕获
  2. ErrorBoundaryB 显示 fallback
  3. Layout、App 正常渲染

如果 ErrorBoundaryB 不存在:

  1. 向上冒泡到 Layout → 不是 Error Boundary,继续冒泡
  2. 到 ErrorBoundaryA → 捕获
  3. ErrorBoundaryA 显示 fallback
  4. 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>
  1. UserProfile throw → ErrorBoundaryB 损获
  2. 渲染 BrokenFallback → BrokenFallback 也 throw
  3. ErrorBoundaryB 自己也挂了 → 向上冒泡
  4. 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 的关系

LawError 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 的核心设计:

  1. 它是 Ownership 的体现:谁拥有这个子树,谁负责处理它的渲染错误
  2. 它是同步的:只能捕获 render 阶段的错误,事件处理器和异步代码自己处理
  3. 它是冒泡的:错误向上冒泡,直到被最近的 Error Boundary 捕获
  4. 它是可组合的:可以嵌套,内层边界优先捕获

最终目标:

让局部失败不影响全局,让用户尽可能看到有意义的内容。

这就是 Error Boundary 的 Ownership 哲学。