~/ ?.log $
返回文章列表
11 min read
更新于 2026年3月6日

我不知道的 React(11)— ErrorBoundary 的工作原理与局限性

React 应用中,一个子组件的渲染错误如果没有被捕获,会导致整棵组件树卸载——也就是白屏。从 React 16 开始,这个行为是刻意设计的:React 团队认为,显示一个损坏的 UI 比完全不显示更危险,因为损坏的 UI 可能导致用户做出错误操作(比如支付页面上金额显示错误)。

React 应用中,一个子组件的渲染错误如果没有被捕获,会导致整棵组件树卸载——也就是白屏。从 React 16 开始,这个行为是刻意设计的:React 团队认为,显示一个损坏的 UI 比完全不显示更危险,因为损坏的 UI 可能导致用户做出错误操作(比如支付页面上金额显示错误)。

ErrorBoundary 是 React 提供的错误隔离机制。但很多人对它的理解停留在”捕获错误、显示兜底 UI”这个层面,实际上它有一条非常明确的能力边界——有些错误它能捕获,有些它根本捕获不了,而这条边界是由 React 的渲染机制决定的。

一、ErrorBoundary 的实现:两个生命周期方法

ErrorBoundary 不是一个特殊的 API 或组件类型,它就是一个普通的类组件,只要实现了 static getDerivedStateFromErrorcomponentDidCatch(或两者都有),就成为了一个 ErrorBoundary。

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('捕获到错误:', error);
    console.error('组件栈:', errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h2>页面出了点问题</h2>;
    }
    return this.props.children;
  }
}

这两个方法的分工有一个很多人会忽略的细节。

getDerivedStateFromError 在 React 的渲染阶段(Render Phase)调用。它是一个静态方法、纯函数,唯一的作用是根据错误返回一个新的 state,触发 re-render 来显示兜底 UI。它不能包含副作用——不能调 API、不能打日志、不能操作 DOM。

componentDidCatch提交阶段(Commit Phase)调用,此时兜底 UI 已经渲染到了 DOM 上。这里才是执行副作用的地方——上报错误到监控平台、打印日志、记录用户上下文信息。

说白了,getDerivedStateFromError 负责”决定渲染什么”,componentDidCatch 负责”渲染完之后做什么”。这个分工和 React Fiber 架构的两阶段设计是一致的(还记得第 02 篇讲的渲染阶段和提交阶段吗?)。渲染阶段可能被暂停和恢复(并发模式下),所以不能有副作用;提交阶段是一次性完成的,适合执行副作用。

二、错误冒泡的路径

当一个子组件在渲染期间抛出错误时,React 的 Fiber 协调器会捕获这个错误,然后沿着 Fiber 树的 return 指针(指向父 Fiber 节点)向上查找,找到最近的 ErrorBoundary。如果找到了,调用它的 getDerivedStateFromErrorcomponentDidCatch,让它渲染兜底 UI。

如果一直向上找都没有找到 ErrorBoundary,整个 React 应用会被卸载。 所以在生产环境中,至少应该在应用的最外层包一个 ErrorBoundary,作为最后的安全网。

下面这个嵌套 ErrorBoundary 的例子可以看出错误隔离的效果:

function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary>
        <MainContent />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}

如果 MainContent 里的某个组件出错了,内层的 ErrorBoundary 会捕获它,只有 MainContent 区域显示兜底 UI,HeaderFooter 不受影响。如果 Header 出错了,错误冒泡到外层 ErrorBoundary,整个页面显示兜底 UI。

ErrorBoundary 的粒度决定了错误隔离的范围。 粒度太粗(只有一个根级 ErrorBoundary),任何错误都会导致整页兜底。粒度太细(每个组件都包一个),代码冗余且维护成本高。实际项目中,按页面区域划分 ErrorBoundary(侧边栏、主内容区、导航栏)是一个比较平衡的策略。

三、ErrorBoundary 的盲区

ErrorBoundary 不是万能的。它有四种错误类型是捕获不了的,而且每一种捕获不了的原因都和 React 的渲染机制有关。

事件处理函数中的错误

function Button() {
  function handleClick() {
    throw new Error('点击出错了');
  }
  return <button onClick={handleClick}>点击</button>;
}

这个错误不会被 ErrorBoundary 捕获。原因在于:事件处理函数不在 React 的渲染流程中执行。 handleClick 是在用户点击后、浏览器的事件调用栈中执行的,此时 React 的渲染阶段早已结束。ErrorBoundary 监听的是渲染阶段(render、构造函数、生命周期方法)中抛出的同步错误,事件回调不在这个范围内。

处理方式是在事件处理函数内部用 try...catch

function Button() {
  const [error, setError] = useState(null);

  function handleClick() {
    try {
      riskyOperation();
    } catch (err) {
      setError(err);
    }
  }

  if (error) {
    return <p>操作失败: {error.message}</p>;
  }
  return <button onClick={handleClick}>点击</button>;
}

异步代码中的错误

setTimeoutPromise.thenasync/await 中的错误同样不会被捕获。原因相同——异步回调的执行时机脱离了 React 的渲染调用栈。当 Promise reject 时,React 的渲染阶段已经结束了,ErrorBoundary 无从介入。

useEffect(() => {
  fetchData()
    .then((data) => setData(data))
    .catch((err) => setError(err));
}, []);

异步错误的处理只能靠 try...catch.catch(),配合 state 来驱动错误 UI 的显示。

ErrorBoundary 自身的错误

如果 ErrorBoundary 的 render 方法或 getDerivedStateFromError 本身抛出了错误,这个错误不会被自己捕获——否则会陷入无限循环。错误会继续向上冒泡,由更上层的 ErrorBoundary 处理。这也是为什么 ErrorBoundary 的代码应该尽量简单——兜底 UI 本身不应该再出错。

服务端渲染期间的错误

ErrorBoundary 是客户端的机制。SSR 过程中的错误需要在服务端的 renderToString / renderToPipeableStream 外层用 try...catch 捕获。

四、为什么 ErrorBoundary 只能用类组件

这是一个高频问题:React 都 Hooks 时代了,为什么 ErrorBoundary 还只能用类组件?

答案是:getDerivedStateFromErrorcomponentDidCatch 没有对应的 Hook 版本。 React 团队目前没有提供 useErrorBoundary 这样的内置 Hook。

原因可能和 Hooks 的执行模型有关。Hooks 在函数组件的函数体中执行,而错误捕获需要在子组件的渲染过程中介入。类组件的生命周期方法是由 React 的 Fiber 协调器在特定阶段调用的,错误捕获可以自然地嵌入到这个流程中。函数组件的 Hook 模型要实现相同的功能,需要不同的设计思路,React 团队可能还在寻找最优方案。

实际项目中的解决办法是使用 react-error-boundary 这个库。它在内部仍然用类组件实现错误捕获,但对外暴露了函数式的 API:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>出错了: {error.message}</p>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 重置应用状态
      }}
    >
      <MainContent />
    </ErrorBoundary>
  );
}

resetErrorBoundary 可以清除错误状态,让 ErrorBoundary 重新渲染子组件树,实现”重试”功能。这比手写类组件的错误恢复逻辑简洁得多。

五、错误上报:从捕获到监控

在生产环境中,仅仅显示兜底 UI 是不够的。componentDidCatch 提供了 errorInfo.componentStack——一个包含错误发生处组件调用栈的字符串,这对于定位问题极其有价值。

componentDidCatch(error, errorInfo) {
  reportToSentry({
    error,
    componentStack: errorInfo.componentStack,
    userContext: getCurrentUserInfo(),
    url: window.location.href,
  });
}

componentStack 长这样:

in BrokenComponent (at App.js:12)
in ErrorBoundary (at App.js:8)
in div (at App.js:7)
in App (at index.js:6)

它展示了从出错组件到根组件的完整路径,和 JavaScript 的 Error stack 互补——Error stack 告诉你哪行代码抛了异常,componentStack 告诉你这行代码在哪个组件层级的上下文中。两者结合,才能快速定位生产环境中的渲染错误。

六、总结

ErrorBoundary 是 React 的错误隔离机制,通过 getDerivedStateFromError(渲染阶段,决定渲染什么)和 componentDidCatch(提交阶段,执行副作用)两个生命周期方法实现。错误沿着 Fiber 树向上冒泡,由最近的 ErrorBoundary 捕获。

它的能力边界很明确:只能捕获子组件树在渲染阶段抛出的同步错误。事件处理函数、异步代码、ErrorBoundary 自身的错误,以及 SSR 期间的错误,都不在它的捕获范围内。理解这条边界,才能设计出完善的错误处理策略——ErrorBoundary 负责渲染错误,try...catch 负责其余的,两者配合才是完整的方案。


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;