说说你在 React 项目中是如何捕获错误的?

一、是什么

在 React 应用中,任何未被捕获的 JavaScript 错误都可能导致整个组件树崩溃,使用户看到白屏。React 16 引入了 Error Boundary(错误边界) 机制,允许开发者在组件层级中优雅地捕获渲染期间的错误,并展示降级 UI,而不是让整个页面挂掉。

错误捕获在 React 中分为两大类场景:

  • 渲染阶段的错误:组件 render、生命周期方法、构造函数中抛出的异常,由 Error Boundary 捕获
  • 非渲染阶段的错误:事件处理函数、异步代码、服务端渲染中的异常,需要传统的 try-catch 等方式处理

二、Error Boundary 错误边界

基本实现

Error Boundary 是一个 class 组件,通过实现 getDerivedStateFromErrorcomponentDidCatch 两个生命周期来捕获子组件树中的错误:

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

  static getDerivedStateFromError(error) {
    // 更新 state,下次渲染展示降级 UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // errorInfo.componentStack 包含出错组件的栈信息
    console.error('组件错误:', error);
    console.error('组件栈:', errorInfo.componentStack);
    // 上报错误到监控平台
    reportErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <h2>页面出错了,请稍后再试</h2>;
    }
    return this.props.children;
  }
}

两个生命周期的分工:

方法触发阶段用途
getDerivedStateFromErrorrender 阶段返回新 state 以渲染降级 UI
componentDidCatchcommit 阶段执行副作用,如错误上报

使用方式

function App() {
  return (
    <ErrorBoundary fallback={<div>应用出错了</div>}>
      <Header />
      <ErrorBoundary fallback={<div>内容加载失败</div>}>
        <MainContent />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}

Error Boundary 的局限性

Error Boundary 无法捕获以下场景的错误:

  1. 事件处理函数中的错误——事件回调不在 React 渲染流程中
  2. 异步代码——如 setTimeoutPromise 回调
  3. 服务端渲染——SSR 阶段没有 commit 过程
  4. Error Boundary 自身抛出的错误
function ButtonWithError() {
  const handleClick = () => {
    // 这个错误 Error Boundary 捕获不到
    throw new Error('点击出错');
  };

  return <button onClick={handleClick}>点击</button>;
}

三、事件处理与异步错误捕获

事件处理函数中的 try-catch

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

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const result = await searchAPI(query);
      setResults(result);
    } catch (err) {
      setError(err.message);
      reportErrorToService(err);
    }
  };

  if (error) {
    return <div className="error-message">{error}</div>;
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

全局未捕获错误监听

// 捕获未处理的同步错误
window.addEventListener('error', (event) => {
  reportErrorToService({
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
  });
});

// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
  reportErrorToService({
    message: event.reason?.message || 'Unhandled Promise rejection',
    stack: event.reason?.stack,
  });
});

四、react-error-boundary 库

社区库 react-error-boundary 提供了更完善的错误边界方案,支持函数式用法和重置机制:

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

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

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

在事件处理中手动触发错误边界

useErrorBoundary hook 可以将事件处理或异步代码中的错误抛给最近的 Error Boundary:

function DataLoader() {
  const { showBoundary } = useErrorBoundary();

  const fetchData = async () => {
    try {
      const res = await fetch('/api/data');
      if (!res.ok) throw new Error('请求失败');
      const data = await res.json();
      setData(data);
    } catch (err) {
      // 将异步错误交给 Error Boundary 处理
      showBoundary(err);
    }
  };

  return <button onClick={fetchData}>加载数据</button>;
}

五、细粒度错误边界策略

实际项目中,不应只在根组件放一个 Error Boundary,而是根据功能模块设置多层错误边界:

function Dashboard() {
  return (
    <div className="dashboard">
      <ErrorBoundary fallback={<HeaderFallback />}>
        <Header />
      </ErrorBoundary>

      <div className="main-layout">
        <ErrorBoundary fallback={<SidebarFallback />}>
          <Sidebar />
        </ErrorBoundary>

        <ErrorBoundary
          fallback={<ContentFallback />}
          onError={logError}
        >
          <MainContent />
        </ErrorBoundary>
      </div>

      <ErrorBoundary fallback={null}>
        <AnalyticsWidget />
      </ErrorBoundary>
    </div>
  );
}

这种策略的好处:

  • 隔离影响范围:某个模块出错不影响其他模块正常使用
  • 差异化降级:核心功能显示友好提示,非核心功能(如统计组件)静默失败
  • 路由级边界:每个路由页面一个 Error Boundary,页面间错误互不影响

六、错误上报与监控

function reportErrorToService(error, errorInfo) {
  const payload = {
    message: error.message,
    stack: error.stack,
    componentStack: errorInfo?.componentStack,
    timestamp: Date.now(),
    url: window.location.href,
    userAgent: navigator.userAgent,
  };

  // 发送到 Sentry / 自建监控平台
  if (typeof window.Sentry !== 'undefined') {
    window.Sentry.captureException(error, {
      contexts: { react: { componentStack: errorInfo?.componentStack } },
    });
  }

  // 或使用 beacon API 确保页面卸载时也能发送
  navigator.sendBeacon('/api/error-log', JSON.stringify(payload));
}

结构化的错误信息应包含:错误消息、调用栈、React 组件栈、页面 URL、用户环境、时间戳等。

七、React 19 的错误处理改进

React 19 对错误处理做了显著改进:

改进的错误报告

React 19 不再重复打印错误日志。之前版本中,createRoot 下的渲染错误会先被 console.error 打印一次,然后 componentDidCatch 再打印一次。React 19 统一为一次有意义的错误输出。

函数组件错误边界的未来

目前 Error Boundary 只能用 class 组件实现,因为依赖 getDerivedStateFromErrorcomponentDidCatch 这两个类生命周期。React 团队已在讨论为函数组件提供等价能力,可能通过新的 hook 或 API 实现。在此之前,react-error-boundary 库是最佳替代方案。

onCaughtError 与 onUncaughtError

React 19 在 createRoot 配置中新增了错误回调:

const root = createRoot(document.getElementById('root'), {
  onCaughtError(error, errorInfo) {
    // Error Boundary 捕获到的错误
    reportErrorToService(error, errorInfo);
  },
  onUncaughtError(error, errorInfo) {
    // 未被 Error Boundary 捕获的错误
    reportErrorToService(error, errorInfo);
    showGlobalErrorDialog(error);
  },
  onRecoverableError(error, errorInfo) {
    // React 自动恢复的错误
    console.warn('Recoverable error:', error);
  },
});

八、总结

错误场景捕获方式
渲染期间(render/生命周期)Error Boundary
事件处理函数try-catch + useState 或 useErrorBoundary
异步代码(Promise/setTimeout)try-catch + showBoundary 或全局监听
全局未捕获错误window.onerror / unhandledrejection
服务端渲染服务端 try-catch + 错误页面

最佳实践:

  • 在应用的关键层级设置多个 Error Boundary,做到错误隔离
  • 使用 react-error-boundary 获得更好的开发体验
  • 建立完善的错误上报机制,结合 Sentry 等监控工具
  • 为用户提供有意义的错误提示和重试选项
  • 关注 React 19 的错误处理新特性,及时升级