React 错误边界

深入理解 React 16 引入的错误边界机制:componentDidCatch 和 getDerivedStateFromError 的区别、错误边界的限制场景、生产环境实践。

React 错误边界

为什么需要错误边界

在 React 的传统模型中,组件内部的 JavaScript 错误会导致 React 的内部状态损坏,并且这个错误会向上冒泡,导致整个组件树崩溃。这种"全有或全无"的失败模式对用户体验是毁灭性的——一个按钮的错误可能导致整个页面白屏。

错误边界(Error Boundary)是 React 16 引入的组件级错误处理机制,它能够捕获子组件树中的 JavaScript 错误,记录错误日志,并显示备用 UI 而不是让组件树崩溃。

面试定位:错误边界是 React 面试中考察"异常处理能力"的代表性话题。面试官通过候选人是否能说清楚 componentDidCatch 和 getDerivedStateFromError 的区别、是否能列举错误边界的限制场景,来判断其对 React 错误处理模型的深度理解。


一、错误边界的基本概念

1.1 定义

错误边界是一个 React 组件,它通过捕获子组件树中的错误来防止整个应用崩溃。错误边界不是

  • 错误边界不能捕获事件处理器中的错误(onClick、onChange 等)
  • 错误边界不能捕获异步代码中的错误(setTimeout、Promise)
  • 错误边界不能捕获服务端渲染中的错误
  • 错误边界自身抛出的错误(它自己崩溃了,那就真的崩溃了)

错误边界可以捕获:

  • 渲染过程中的错误
  • 生命周期方法中的错误
  • 构造函数中的错误
  • 子组件的错误

1.2 最小可用的错误边界

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示备用 UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误日志
    console.error('ErrorBoundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

使用方式:

<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

1.3 两个生命周期方法的职责

错误边界使用两个生命周期方法来处理错误,它们有不同的适用场景:

getDerivedStateFromError

  • 静态方法,在渲染阶段调用
  • 返回一个新的 state 对象来触发备用 UI 渲染
  • 因为在渲染阶段调用,所以不能有副作用(不能调用 fetch、不能调用 console.error 之外的方法)
  • 用于决定渲染什么样的备用 UI

componentDidCatch

  • 实例方法,在提交(commit)阶段调用
  • 可以有副作用(调用 fetch、console.error、发送错误报告到服务器)
  • 用于错误日志记录和错误监控
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    // ✅ 用于更新状态(渲染备用 UI)
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // ✅ 用于副作用(记录日志、发送到监控服务)
    errorReporter(error, errorInfo);
    logger.log('ErrorBoundary caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 备用 UI
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

二、componentDidCatch vs getDerivedStateFromError

2.1 核心区别

维度 getDerivedStateFromError componentDidCatch
调用时机 渲染阶段 提交(commit)阶段
方法类型 静态方法 实例方法
是否有副作用 禁止(有严格检查) 允许
主要用途 计算新的 state 记录错误日志
能否访问 this 不能

2.2 为什么需要两个方法

React 的设计团队在实现错误边界时面临一个权衡:渲染阶段的错误是否应该允许副作用?

最终他们采用了分工模式:

  1. 渲染阶段——getDerivedStateFromError:决定是否显示备用 UI,这个决定必须同步做出,所以不能有副作用
  2. 提交阶段——componentDidCatch:完成渲染后,可以安全地进行副作用操作(如发送错误报告)

这种设计确保了 React 的可预测性:渲染阶段是纯函数式的(输入 state + props,输出 UI),提交阶段才允许副作用。

2.3 React 17 的变化

React 17 改进了错误边界的实现,但没有改变 API。主要变化是:

  • 错误报告机制更加健壮
  • 同一个错误不会被多次报告
  • 与 React DevTools 的集成更好

值得注意的是,React 17 之前错误会被上报到 window.onerror,React 17 之后改为上报到 window.addEventListener('error', ...) 的处理器。这是实现细节的变化,不影响使用方式。


三、错误边界的限制场景

3.1 事件处理器

事件处理器中的错误无法被错误边界捕获

function MyComponent() {
  const handleClick = () => {
    // 这个错误无法被错误边界捕获
    throw new Error('Click error!');
  };

  return <button onClick={handleClick}>Click me</button>;
}

// 正确做法:使用 try-catch
function MyComponent() {
  const handleClick = () => {
    try {
      // 可能抛出错误的代码
      riskyOperation();
    } catch (err) {
      handleError(err);
    }
  };

  return <button onClick={handleClick}>Click me</button>;
}

这是因为 React 的事件系统不通过错误边界机制处理——事件处理器在 React 的控制流之外执行。

3.2 异步代码

setTimeout、Promise、requestAnimationFrame 等异步代码中的错误也无法被错误边界捕获:

function MyComponent() {
  useEffect(() => {
    // 这个错误无法被错误边界捕获
    setTimeout(() => {
      throw new Error('Async error!');
    }, 1000);
  }, []);

  return <div>Async error demo</div>;
}

// 正确做法:使用 try-catch + .catch()
useEffect(() => {
  fetchData()
    .then(processData)
    .catch(handleError); // 捕获 Promise rejection
}, []);

3.3 服务端渲染

在服务端渲染(SSR)期间发生的错误无法被错误边界捕获,因为 SSR 环境中没有 React 的组件树结构来应用错误边界。通常的解决方案是:

  • 在服务端使用 try-catch 包裹
  • 使用错误监控服务(如 Sentry)捕获 SSR 错误
  • Next.js 提供了 getInitialPropsgetServerSideProps 等的安全机制

3.4 错误边界自身

如果错误边界的构造函数或渲染方法本身抛出错误,这个错误会向上冒泡到最近的父错误边界。如果没有任何错误边界能够处理,应用会完全崩溃。

class BrokenBoundary extends React.Component {
  constructor(props) {
    super(props);
    // 如果这里抛出错误,错误边界本身就会崩溃
    throw new Error('Boundary constructor error!');
  }

  render() {
    return this.props.children;
  }
}

四、错误边界的部署策略

4.1 放置位置决策

错误边界的放置位置需要权衡防护范围用户体验

方案一:每个组件一个错误边界

<ErrorBoundary>
  <Sidebar />
</ErrorBoundary>
<ErrorBoundary>
  <Content />
</ErrorBoundary>
<ErrorBoundary>
  <Footer />
</ErrorBoundary>
  • 优点:最小化崩溃范围,一个组件崩溃不影响其他组件
  • 缺点:备用 UI 碎片化,用户体验可能不一致

方案二:区域性错误边界

<ErrorBoundary>
  <Header />
  <main>
    <ErrorBoundary>
      <Article />
    </ErrorBoundary>
    <ErrorBoundary>
      <Comments />
    </ErrorBoundary>
  </main>
  <ErrorBoundary>
    <Footer />
  </ErrorBoundary>
</ErrorBoundary>
  • 优点:平衡了防护范围和用户体验
  • 缺点:实现复杂度增加

方案三:全局错误边界

<ErrorBoundary>
  <App />
</ErrorBoundary>
  • 优点:实现简单,确保应用不会完全白屏
  • 缺点:任何错误都显示同一个备用 UI,可能让用户困惑

4.2 分层错误边界实践

实际生产环境中,推荐分层策略

第一层:组件级(细粒度)

function Button({ onClick, children }) {
  return (
    <ErrorBoundary fallback={<ButtonFallback />}>
      <button onClick={onClick}>{children}</button>
    </ErrorBoundary>
  );
}

第二层:区域级(中粒度)

function Dashboard() {
  return (
    <ErrorBoundary fallback={<DashboardErrorFallback />}>
      <MetricsPanel />
      <ChartPanel />
      <NotificationPanel />
    </ErrorBoundary>
  );
}

第三层:应用级(粗粒度)

function App() {
  return (
    <GlobalErrorBoundary fallback={<GlobalErrorFallback />}>
      <Router />
    </GlobalErrorBoundary>
  );
}

这种分层策略的好处是:常见组件错误由细粒度边界处理,区域性错误由中粒度边界处理,只有最严重的错误才触发全局备用 UI。

4.3 备用 UI 设计

备用 UI 的设计需要考虑:

  1. 不要直接暴露错误详情给用户:错误堆栈信息只应该记录到日志,对用户显示友好的说明
  2. 提供恢复路径:如果可能,提供一个"重试"或"刷新"按钮
  3. 保持上下文:让用户知道当前在应用的什么位置
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

  handleRetry = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <AlertCircleIcon />
          <h2>内容加载失败</h2>
          <p>抱歉,此部分内容暂时无法显示。</p>
          <button onClick={this.handleRetry}>重试</button>
          <button onClick={() => window.location.reload()}>刷新页面</button>
        </div>
      );
    }
    return this.props.children;
  }
}

五、生产环境实践

5.1 错误监控集成

在生产环境中,componentDidCatch 是发送错误报告的理想位置:

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    // 发送到错误监控服务
    errorReporter.captureException(error, {
      extra: errorInfo,
      tags: {
        component: this.props.fallback || 'anonymous',
      },
    });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

常用的错误监控服务:

  • Sentry:最流行的前端错误监控,支持 React 集成
  • LogRocket:除了错误还支持 session 回放
  • Datadog:企业级监控

5.2 错误边界的 TypeScript 类型

interface ErrorInfo {
  componentStack: string;
}

interface Props {
  children: React.ReactNode;
  fallback?: React.ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<Props, State> {
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorFallback />;
    }
    return this.props.children;
  }
}

5.3 HOC 版本的错误边界

为了在更多场景中复用错误边界,可以创建一个 HOC(高阶组件):

function withErrorBoundary<P>(
  WrappedComponent: React.ComponentType<P>,
  fallback: React.ReactNode,
  errorCallback?: (error: Error, errorInfo: ErrorInfo) => void
) {
  return class extends React.Component<P> {
    static displayName = `withErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name})`;

    render() {
      return (
        <ErrorBoundary fallback={fallback} onError={errorCallback}>
          <WrappedComponent {...this.props} />
        </ErrorBoundary>
      );
    }
  };
}

// 使用
const ProtectedChart = withErrorBoundary(
  Chart,
  <ChartErrorFallback />,
  (error) => sendToMonitor(error)
);

5.4 React 18 的 Error Boundary 和 Suspense

React 18 引入了新的错误处理模式——结合 Suspense 的 Error Boundary:

<ErrorBoundary fallback={<ErrorFallback />}>
  <Suspense fallback={<Loading />}>
    <AsyncComponent />
  </Suspense>
</ErrorBoundary>

在这个模式中:

  • Suspense 负责处理加载状态
  • Error Boundary 负责处理错误状态
  • 两者配合实现了完整的"加载→成功/失败"状态机

React 18 的 use Hook 进一步改进了这一模式——它允许在组件内部读取 Promise 的结果,并且支持与 Error Boundary 集成。


六、面试高频问题

Q: componentDidCatch 和 getDerivedStateFromError 的区别是什么?

回答要点:两者都是错误边界的生命周期方法,但有根本区别。getDerivedStateFromError 是静态方法,在渲染阶段调用,用于计算新的 state 来触发备用 UI 渲染,因为处于渲染阶段所以不能有副作用。componentDidCatch 是实例方法,在提交阶段调用,可以有副作用,适合记录错误日志和发送错误报告到监控服务。实际应用中通常同时使用两者——前者处理 UI 状态,后者处理错误报告。

Q: 错误边界能捕获哪些错误?哪些不能?

回答要点:错误边界能捕获:渲染错误、生命周期方法错误、构造函数错误。但有明确限制——事件处理器中的错误(需要 try-catch)、异步代码中的错误(setTimeout、Promise rejection,需要 .catch 或 try-catch)、服务端渲染错误(需要 SSR 层面的异常处理)、错误边界自身抛出的错误。这些限制的根本原因是 React 的错误边界机制只在组件渲染和生命周期方法中生效,超出这个范围的就是普通 JavaScript 执行上下文。

Q: 在 React 16 之前是怎么处理组件错误的?

回答要点:React 16 之前没有错误边界机制,组件内部的 JavaScript 错误会向上冒泡,导致整个 React 应用崩溃。开发者通常用几种方式规避:使用 try-catch 包裹每个可能出错的地方、使用事件处理器层面的错误处理(onerror)、使用 Promise 的 .catch()。但这些方式都无法处理渲染错误,而且会导致大量重复的样板代码。错误边界的引入解决了这个问题——将错误处理逻辑集中在组件层面,让 React 自动处理错误的冒泡和捕获。


延展阅读