React 中 componentDidCatch 捕获错误日志

FreeGuideOnline 最新 2026-07-07

什么是 React 错误边界

在构建复杂的 React 应用时,一个组件中的 JavaScript 错误可能会破坏整个 UI。为了避免这种情况,React 16 引入了**错误边界(Error Boundaries)**的概念。错误边界是一种 React 组件,它可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并展示出备用的降级 UI,而不是让整个应用崩溃。

错误边界就像一个 try...catch 语句,但它是为声明式渲染而设计的。它无法捕获自身内部的错误,而是专门捕获其子组件在渲染过程、生命周期方法以及构造函数中抛出的错误。

componentDidCatch 方法详解

componentDidCatch 是实现错误边界的关键生命周期方法。当后代组件抛出错误后,该方法会被调用,你可以在其中执行副作用操作,最常见的就是错误日志的记录

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

  static getDerivedStateFromError(error) {
    // 更新 state,以便下一次渲染能够显示降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你可以在这里将错误日志上报给服务端
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级 UI
      return <h1>出现问题请稍后再试</h1>;
    }

    return this.props.children;
  }
}

参数解析

componentDidCatch(error, errorInfo) 接收两个参数:

  • error:被抛出的错误对象,通常是一个 Error 实例,包含 messagestack 等属性。
  • errorInfo:一个包含 componentStack 属性的对象,该属性记录了错误发生时组件堆栈的追踪信息。注意:从 React 17 开始,componentStack 已改为在 errorInfo 上直接作为第一个参数抛出,标准写法是 componentDidCatch(error, errorInfo),但部分版本可能略有差异,请以最新官方文档为准。

错误信息通常包含以下关键数据:

componentDidCatch(error, errorInfo) {
  // 错误消息,例如: "Cannot read property 'map' of undefined"
  console.log(error.message);
  // 完整的错误堆栈
  console.log(error.stack);
  // 组件堆栈,显示错误发生在哪个组件层级
  console.log(errorInfo.componentStack);
}

如何使用错误边界捕获错误日志

1. 创建错误边界组件

首先创建一个可复用的 ErrorLoggerBoundary 组件,用于包裹可能出错的子组件。

import React from 'react';

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

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

  componentDidCatch(error, errorInfo) {
    // 构建要记录的日志对象
    const logData = {
      errorMessage: error.message,
      errorStack: error.stack,
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
    };

    // 发送给日志收集接口(例如 Sentry、自建服务等)
    this.sendErrorLog(logData);
  }

  sendErrorLog(logData) {
    fetch('/api/log-error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(logData),
    }).catch(err => {
      // 如果日志上报本身失败,不要再次抛出,避免死循环
      console.warn('日志上报失败', err);
    });
  }

  render() {
    if (this.state.hasError) {
      return <div className="error-fallback">组件渲染异常我们已记录该问题</div>;
    }
    return this.props.children;
  }
}

export default ErrorLoggerBoundary;

2. 在应用中部署错误边界

然后将 ErrorLoggerBoundary 包裹在可能抛出错误的组件周围。通常,我们会针对页面级或模块级区域设置错误边界,而不是在每个小组件中都包裹,以保持合理的粒度。

function App() {
  return (
    <div>
      <Header />
      {/* 为侧边栏设置独立的错误边界 */}
      <ErrorLoggerBoundary>
        <Sidebar />
      </ErrorLoggerBoundary>
      {/* 为主内容区设置错误边界 */}
      <ErrorLoggerBoundary>
        <MainContent />
      </ErrorLoggerBoundary>
      <Footer />
    </div>
  );
}

3. 模拟错误以进行测试

你可以故意在子组件中抛出一个错误,来验证错误边界和日志上报是否正常工作。

function BuggyCounter() {
  const [count, setCount] = React.useState(0);

  if (count === 5) {
    // 当计数达到5时,模拟一个错误
    throw new Error('计数器崩溃!');
  }

  return <button onClick={() => setCount(count + 1)}>点击 {count}</button>;
}

// 使用
<ErrorLoggerBoundary>
  <BuggyCounter />
</ErrorLoggerBoundary>

当点击按钮 5 次后,错误边界会捕获到错误,componentDidCatch 将执行日志上报,同时 UI 会显示备用的降级内容。

错误日志的最佳实践

只记录,不恢复

componentDidCatch 主要用于副作用,例如日志记录。不要在此方法中尝试通过 setState 来恢复渲染,因为错误边界已经通过 getDerivedStateFromError 切换到降级状态。如果你确实需要重置错误状态,可以让用户手动点击“重试”按钮,或者在特定条件下(例如路由变化时)重置。

区分开发环境与生产环境

在开发模式下,React 会在错误边界捕获错误之前将该错误打印到控制台,以便你调试。在生产环境中,你应该确保日志系统平稳运行,避免因为日志上报本身的问题导致用户端崩溃。可以给日志发送函数加上 try...catch,并采用非阻塞的方式(如 navigator.sendBeacon 或异步请求)。

使用 sendBeacon 示例:

componentDidCatch(error, errorInfo) {
  const logData = {
    error: error.message,
    componentStack: errorInfo.componentStack,
  };
  const blob = new Blob([JSON.stringify(logData)], { type: 'application/json' });
  navigator.sendBeacon('/api/log-error', blob);
}

收集有意义的上下文信息

单纯的错误消息往往不足以快速定位问题。建议在日志中附带以下信息:

  • 当前 URL(window.location.href
  • 用户标识(如果已登录)
  • React 版本
  • 导致错误的确切 props/state(如果可能)
  • 时间戳和浏览器信息

与第三方监控工具集成

手动实现日志收集通常不如现成的错误监控服务强大。你可以选择集成 SentryLogRocketDatadog RUM 等工具。这些服务提供了丰富的功能,如错误分组、源代码映射和完整的会话回放。

以 Sentry 为例:

import * as Sentry from '@sentry/react';

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    Sentry.withScope(scope => {
      scope.setExtras(errorInfo);
      Sentry.captureException(error);
    });
  }
  // ...
}

注意无法捕获的场景

错误边界不能捕获以下类型的错误:

  • 事件处理器中的错误(你需要使用普通的 try...catch
  • 异步代码(例如 setTimeoutrequestAnimationFrame 回调)
  • 服务端渲染过程中的错误
  • 错误边界自身抛出的错误

对于这些情况,仍需配合全局的 window.onerrorwindow.addEventListener('unhandledrejection') 进行兜底。

componentDidCatchErrorBoundary 的完整范例

下面是一个可直接运行的完整示例,它创建了一个错误边界,并记录了所有捕获到的错误。

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // 更新状态,下次渲染时展示回退 UI
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误详情
    console.error('捕获到错误:', error, errorInfo);
    this.logErrorToServer(error, errorInfo);
  }

  logErrorToServer(error, errorInfo) {
    const payload = {
      message: error.toString(),
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      url: window.location.href,
      timestamp: new Date().toISOString(),
    };

    // 使用 sendBeacon 确保页面卸载时也能发送
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/error-log', new Blob([JSON.stringify(payload)], { type: 'application/json' }));
    } else {
      // 降级为普通 fetch
      fetch('/api/error-log', {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: { 'Content-Type': 'application/json' },
      }).catch(() => {});
    }
  }

  render() {
    if (this.state.error) {
      // 自定义回退 UI,可以包含错误信息(生产环境中避免暴露细节)
      return (
        <div>
          <h2>很抱歉出现了一些问题</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo?.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

常见问题解答 (FAQ)

1. componentDidCatchstatic getDerivedStateFromError 有什么区别?

getDerivedStateFromError 用于在渲染阶段更新 state,以便显示降级 UI。它是纯函数,不应包含副作用。而 componentDidCatch 则专门用于执行副作用,如日志记录。两者结合使用,一个负责 UI 展示,一个负责日志收集。

2. 类组件才能使用错误边界,函数组件可以吗?

目前,只有类组件可以成为错误边界。函数组件无法直接使用 componentDidCatchgetDerivedStateFromError。但你可以使用第三方库(如 react-error-boundary)提供的包装组件,或者自己封装一个类组件边界,然后在函数组件中使用它。

3. 应该在哪里放置错误边界?

一个常用的策略是在路由切换的顶层包裹一个错误边界,这样即使某个页面崩溃,其他页面仍然可用。同时,对于第三方组件或容易出错的独立模块(如复杂图表、编辑器)也可以单独包裹。

4. 生产环境中是否应该显示错误详情?

出于安全考虑,生产环境的降级 UI 通常不应展示具体的错误消息或堆栈,以免泄露技术细节。错误详情应仅发送到日志服务器。

小结

componentDidCatch 是 React 错误边界机制的核心,它为开发者提供了在组件树崩溃时优雅记录错误日志并展示备用 UI 的能力。正确配置错误边界,结合完善的日志记录方案,可以大幅提升应用在生产环境中的稳定性和可维护性。建议在项目的关键节点(页面级、功能模块级)落实错误边界,并配合现代化的错误监控平台,打造健壮的用户体验。