React 错误边界处理

错误边界(Error Boundaries)是React中捕获和处理组件错误的机制,可以防止因局部错误导致整个应用崩溃。

什么是错误边界?

错误边界是React的一种错误处理机制,允许你捕获子组件树中任意位置的JavaScript错误,并显示降级UI而不是崩溃的组件树。

错误发生

子组件抛出错误

错误捕获

错误边界捕获错误

状态更新

getDerivedStateFromError

错误记录

componentDidCatch

降级显示

显示备用UI

创建错误边界组件

import React from 'react';

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

  // 静态方法:更新state使下一次渲染显示降级UI
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error: error,
      errorTimestamp: new Date()
    };
  }

  // 实例方法:捕获错误并记录错误信息
  componentDidCatch(error, errorInfo) {
    // 记录错误到控制台
    console.error('组件错误:', error);
    console.error('错误堆栈:', errorInfo.componentStack);

    // 可以发送错误到错误监控服务
    this.logErrorToService(error, errorInfo);

    // 更新state以显示错误详情(可选)
    this.setState({
      errorInfo: errorInfo
    });
  }

  logErrorToService(error, errorInfo) {
    // 发送错误到错误监控服务(如Sentry, LogRocket等)
    if (window.errorLoggingService) {
      window.errorLoggingService.log({
        error: error.toString(),
        stack: errorInfo.componentStack,
        url: window.location.href,
        timestamp: new Date().toISOString()
      });
    }
  }

  // 重置错误状态
  resetError = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null
    });
  };

  // 重试加载组件
  retry = () => {
    this.resetError();
    // 如果有重试回调,则调用
    if (this.props.onRetry) {
      this.props.onRetry();
    }
  };

  render() {
    if (this.state.hasError) {
      // 显示降级UI
      return (
        <div className="error-boundary-ui">
          <div className="error-content">
            <h3>⚠️ 组件发生错误</h3>
            <p>抱歉,我们遇到了一个问题。</p>

            {this.props.showDetails && this.state.error && (
              <div className="error-details">
                <p><strong>错误信息:</strong> {this.state.error.toString()}</p>
                {this.state.errorInfo && (
                  <pre className="error-stack">
                    {this.state.errorInfo.componentStack}
                  </pre>
                )}
              </div>
            )}

            <div className="error-actions">
              <button onClick={this.retry} className="retry-btn">
                重试
              </button>
              <button onClick={this.resetError} className="reset-btn">
                重置
              </button>
              {this.props.onReport && (
                <button onClick={() => this.props.onReport(this.state.error)}>
                  报告问题
                </button>
              )}
            </div>
          </div>
        </div>
      );
    }

    // 正常情况下渲染子组件
    return this.props.children;
  }
}

// 设置默认props
ErrorBoundary.defaultProps = {
  showDetails: process.env.NODE_ENV === 'development'
};

// 使用示例
function App() {
  return (
    <ErrorBoundary
      showDetails={true}
      onRetry={() => console.log('正在重试...')}
      onReport={(error) => alert(`报告错误: ${error}`)}
    >
      <BuggyComponent />
    </ErrorBoundary>
  );
}
错误边界演示 正常

安全组件区域

这个组件被错误边界保护着。

错误边界使用场景

组件级
独立组件错误

保护独立的UI组件,防止单个组件错误影响其他部分

<ErrorBoundary>
  <UserProfile />
</ErrorBoundary>

<ErrorBoundary>
  <DashboardWidget />
</ErrorBoundary>
路由级
页面路由保护

在路由级别保护整个页面,提供页面级错误处理

<Route
  path="/dashboard"
  element={
    <ErrorBoundary fallback={<DashboardErrorPage />}>
      <DashboardPage />
    </ErrorBoundary>
  }
/>
数据获取
API错误处理

处理数据获取失败,显示重试按钮或备选内容

<ErrorBoundary
  onRetry={fetchData}
  fallback={<DataLoadingError />}
>
  <DataComponent data={data} />
</ErrorBoundary>

错误边界与函数组件

由于错误边界必须是类组件,在函数组件中需要通过HOC(高阶组件)或自定义Hook来使用:

import React, { useEffect, useState } from 'react';

// 高阶组件:为函数组件添加错误边界
function withErrorBoundary(WrappedComponent, FallbackComponent) {
  return class extends React.Component {
    state = { hasError: false, error: null };

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

    componentDidCatch(error, errorInfo) {
      console.error('Error in component:', error);
    }

    render() {
      if (this.state.hasError) {
        return FallbackComponent ?
          <FallbackComponent error={this.state.error} /> :
          <DefaultErrorFallback error={this.state.error} />;
      }

      return <WrappedComponent {...this.props} />;
    }
  };
}

// 使用高阶组件
const SafeComponent = withErrorBoundary(BuggyFunctionComponent, ErrorFallback);

// 自定义Hook:模拟错误边界效果
function useErrorBoundary() {
  const [hasError, setHasError] = useState(false);
  const [error, setError] = useState(null);

  const handleError = (error) => {
    setHasError(true);
    setError(error);
    console.error('Caught error:', error);
  };

  const resetError = () => {
    setHasError(false);
    setError(null);
  };

  return {
    hasError,
    error,
    handleError,
    resetError
  };
}

// 在函数组件中使用
function FunctionComponentWithErrorHandling() {
  const { hasError, error, handleError, resetError } = useErrorBoundary();

  useEffect(() => {
    try {
      // 可能会抛出错误的操作
      riskyOperation();
    } catch (err) {
      handleError(err);
    }
  }, []);

  if (hasError) {
    return (
      <div>
        <p>组件发生错误: {error?.message}</p>
        <button onClick={resetError}>重试</button>
      </div>
    );
  }

  return <div>正常内容</div>;
}

错误处理演示

不同类型错误演示

错误日志控制台
点击上方按钮触发不同类型错误
渲染错误

✅ 正常

事件错误

✅ 正常

异步错误

✅ 正常

错误边界的限制

无法捕获的错误类型
  • 事件处理函数中的错误:需要在事件处理函数内部使用try/catch
  • 异步代码错误:setTimeout、Promise、async/await中的错误
  • 服务端渲染错误:错误边界只在客户端渲染时工作
  • 错误边界自身的错误:错误边界不能捕获自身的错误
处理异步错误
// 异步错误无法被错误边界捕获
useEffect(() => {
  // ❌ 不会被错误边界捕获
  fetch('/api/data')
    .then(() => { throw new Error('Async error'); });

  // ✅ 需要单独处理
  fetch('/api/data')
    .then(() => { throw new Error('Async error'); })
    .catch(error => {
      // 手动处理错误
      setError(error);
      // 或者调用错误处理函数
      onError(error);
    });
}, []);

// 使用错误边界包装异步操作
class AsyncErrorBoundary extends React.Component {
  state = { error: null };

  componentDidCatch(error, errorInfo) {
    this.setState({ error });
  }

  handleAsyncError = (error) => {
    this.setState({ error });
  };

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

    return React.cloneElement(this.props.children, {
      onError: this.handleAsyncError
    });
  }
}
处理事件处理函数错误
function ComponentWithEvent() {
  const [error, setError] = useState(null);

  const handleClick = () => {
    try {
      // 可能抛出错误的操作
      riskyOperation();
    } catch (err) {
      // 1. 在事件处理函数中捕获错误
      setError(err);
      // 2. 可以记录到错误服务
      logError(err);
      // 3. 显示用户友好的错误信息
      alert('操作失败,请重试');
    }
  };

  const handleClickWithBoundary = () => {
    try {
      riskyOperation();
    } catch (err) {
      // 抛出错误让错误边界捕获
      throw err;
    }
  };

  if (error) {
    return <ErrorDisplay error={error} onRetry={() => setError(null)} />;
  }

  return (
    <div>
      <button onClick={handleClick}>安全点击</button>
      <ErrorBoundary>
        <button onClick={handleClickWithBoundary}>
          使用错误边界
        </button>
      </ErrorBoundary>
    </div>
  );
}

高级错误边界模式

1. 嵌套错误边界

// 嵌套错误边界可以提供更细粒度的错误处理
function App() {
  return (
    <ErrorBoundary fallback={<AppCrashPage />}>
      <Header />

      <main>
        <ErrorBoundary fallback={<SidebarError />}>
          <Sidebar />
        </ErrorBoundary>

        <ErrorBoundary fallback={<ContentError />}>
          <Content>
            <ErrorBoundary fallback={<WidgetError />}>
              <DashboardWidget />
            </ErrorBoundary>

            <ErrorBoundary fallback={<ChartError />}>
              <DataChart />
            </ErrorBoundary>
          </Content>
        </ErrorBoundary>
      </main>

      <ErrorBoundary fallback={null}> {/* 静默失败 */}
        <Footer />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

2. 带重试功能的错误边界

class RetryErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      retryCount: 0,
      maxRetries: props.maxRetries || 3
    };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error);
  }

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

  render() {
    if (this.state.hasError) {
      // 检查是否超过重试次数
      if (this.state.retryCount >= this.state.maxRetries) {
        return this.props.fallback || (
          <div className="max-retries-exceeded">
            <h3>重试次数已用尽</h3>
            <p>请稍后再试或联系支持</p>
          </div>
        );
      }

      return (
        <div className="retry-error-boundary">
          <h3>加载失败</h3>
          <p>重试次数: {this.state.retryCount}/{this.state.maxRetries}</p>
          <button onClick={this.handleRetry}>
            重试 ({this.state.retryCount + 1}/{this.state.maxRetries})
          </button>
          {this.props.onRetry && (
            <button onClick={() => this.props.onRetry(this.state.error)}>
              报告问题
            </button>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用示例
<RetryErrorBoundary
  maxRetries={5}
  fallback={<PermanentErrorPage />}
  onRetry={(error) => reportError(error)}
>
  <UnstableComponent />
</RetryErrorBoundary>

3. 上下文错误边界

import React, { createContext, useContext, useState } from 'react';

// 创建错误上下文
const ErrorContext = createContext();

// 错误提供者组件
function ErrorProvider({ children }) {
  const [errors, setErrors] = useState([]);
  const [globalError, setGlobalError] = useState(null);

  const addError = (error, errorInfo) => {
    const errorEntry = {
      id: Date.now(),
      error,
      errorInfo,
      timestamp: new Date().toISOString(),
      url: window.location.href
    };

    setErrors(prev => [...prev, errorEntry]);

    // 发送到错误监控服务
    sendToErrorService(errorEntry);
  };

  const clearError = (errorId) => {
    setErrors(prev => prev.filter(err => err.id !== errorId));
  };

  const clearAllErrors = () => {
    setErrors([]);
    setGlobalError(null);
  };

  const sendToErrorService = (errorEntry) => {
    // 发送到Sentry, LogRocket等
    console.log('Error logged:', errorEntry);
  };

  return (
    <ErrorContext.Provider value={{
      errors,
      globalError,
      addError,
      clearError,
      clearAllErrors,
      setGlobalError
    }}>
      {children}
    </ErrorContext.Provider>
  );
}

// 使用上下文的错误边界
class ContextErrorBoundary extends React.Component {
  static contextType = ErrorContext;

  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error, errorInfo) {
    // 使用上下文记录错误
    if (this.context.addError) {
      this.context.addError(error, errorInfo);
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div>
          <h3>组件错误</h3>
          <p>错误已被记录</p>
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ErrorProvider>
      <ContextErrorBoundary fallback={<AppErrorPage />}>
        <YourApp />
      </ContextErrorBoundary>
    </ErrorProvider>
  );
}

错误监控与报告

// 集成Sentry错误监控
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

// 初始化Sentry
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  integrations: [new BrowserTracing()],
  tracesSampleRate: 1.0,
});

// 创建Sentry错误边界
const SentryErrorBoundary = Sentry.ErrorBoundary;

// 使用
<SentryErrorBoundary
  fallback={<ErrorFallback />}
  onError={(error, errorInfo) => {
    console.log('Error sent to Sentry');
  }}
>
  <YourComponent />
</SentryErrorBoundary>

// 自定义错误报告
class ReportingErrorBoundary extends React.Component {
  state = { hasError: false };

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

  componentDidCatch(error, errorInfo) {
    // 报告错误到不同服务
    this.reportError(error, errorInfo);
  }

  reportError = (error, errorInfo) => {
    const errorData = {
      message: error.toString(),
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
      userId: this.getUserId(), // 获取当前用户ID
      extraData: this.props.extraData
    };

    // 发送到多个错误服务
    this.sendToBackend(errorData);
    this.sendToAnalytics(errorData);
    this.sendToLoggingService(errorData);
  };

  sendToBackend = (errorData) => {
    fetch('/api/log-error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorData)
    }).catch(err => {
      console.error('Failed to send error to backend:', err);
    });
  };

  sendToAnalytics = (errorData) => {
    if (window.gtag) {
      gtag('event', 'error', {
        event_category: 'JavaScript',
        event_label: errorData.message,
        value: 1
      });
    }
  };

  sendToLoggingService = (errorData) => {
    // 发送到第三方日志服务
    console.log('Error logged:', errorData);
  };

  getUserId = () => {
    // 从localStorage、context或state中获取用户ID
    return localStorage.getItem('userId');
  };

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-fallback">
          <h3>出错了</h3>
          <p>我们已经记录了这个错误</p>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

最佳实践

错误边界最佳实践检查表
常见错误处理误区:
  • 过度使用错误边界:只在需要的地方使用,避免性能开销
  • 忽略异步错误:记得处理Promise、async/await中的错误
  • 泄露敏感信息:生产环境不要显示详细错误堆栈
  • 没有错误恢复机制:提供重试或刷新选项
  • 忽略错误监控:记录错误以便分析和修复

错误详情