Skip to content

错误边界基础

第一部分:错误边界概述

1.1 什么是错误边界

错误边界(Error Boundaries)是React组件,用于捕获其子组件树中的JavaScript错误,记录这些错误,并显示备用UI而不是崩溃的组件树。

核心特点:

javascript
// 错误边界是一个类组件
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    // 更新state,下次渲染显示备用UI
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // 记录错误到错误报告服务
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>出错了</h1>;
    }
    
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>
  );
}

1.2 为什么需要错误边界

javascript
// 问题:没有错误边界
function App() {
  return (
    <div>
      <Header />
      <BuggyCounter />  {/* 这里出错会导致整个App崩溃 */}
      <Footer />
    </div>
  );
}

function BuggyCounter() {
  const [count, setCount] = useState(0);
  
  if (count === 3) {
    throw new Error('I crashed!');  // 崩溃!
  }
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// 结果:用户看到白屏,整个应用不可用

// 解决:使用错误边界
function App() {
  return (
    <div>
      <Header />
      
      <ErrorBoundary fallback={<div>计数器出错了</div>}>
        <BuggyCounter />
      </ErrorBoundary>
      
      <Footer />
    </div>
  );
}

// 结果:只有BuggyCounter区域显示错误,其他部分正常

1.3 错误边界的能力范围

javascript
// ✅ 错误边界可以捕获:
// 1. 子组件渲染时的错误
// 2. 生命周期方法中的错误
// 3. 构造函数中的错误

class ProblematicComponent extends React.Component {
  constructor(props) {
    super(props);
    throw new Error('Constructor error');  // ✅ 可以捕获
  }
  
  componentDidMount() {
    throw new Error('Lifecycle error');  // ✅ 可以捕获
  }
  
  render() {
    throw new Error('Render error');  // ✅ 可以捕获
  }
}

// ❌ 错误边界不能捕获:
// 1. 事件处理器中的错误
// 2. 异步代码(setTimeout, requestAnimationFrame)
// 3. 服务端渲染的错误
// 4. 错误边界自身的错误

function NotCaught() {
  const handleClick = () => {
    throw new Error('Event handler error');  // ❌ 不能捕获
  };
  
  useEffect(() => {
    setTimeout(() => {
      throw new Error('Async error');  // ❌ 不能捕获
    }, 1000);
  }, []);
  
  return <button onClick={handleClick}>Click</button>;
}

// 事件处理器需要try-catch
function SafeEventHandler() {
  const [error, setError] = useState(null);
  
  const handleClick = () => {
    try {
      throw new Error('Event error');
    } catch (err) {
      setError(err);
    }
  };
  
  if (error) {
    return <div>错误: {error.message}</div>;
  }
  
  return <button onClick={handleClick}>Click</button>;
}

1.4 基本实现

javascript
// 最简单的错误边界
class SimpleErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children;
  }
}

// 带错误信息的版本
class ErrorBoundaryWithInfo extends React.Component {
  state = { hasError: false, error: null, errorInfo: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    
    // 可选:记录到错误服务
    logErrorToService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>出错了</h2>
          <details>
            <summary>错误详情</summary>
            <p>{this.state.error?.toString()}</p>
            <p>{this.state.errorInfo?.componentStack}</p>
          </details>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 可配置的错误边界
class ConfigurableErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    this.props.onError?.(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback({ 
        error: this.state.error,
        reset: () => this.setState({ hasError: false, error: null })
      });
    }
    
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ConfigurableErrorBoundary
      fallback={({ error, reset }) => (
        <div>
          <h2>Error: {error.message}</h2>
          <button onClick={reset}>重试</button>
        </div>
      )}
      onError={(error, errorInfo) => {
        console.error('Caught error:', error, errorInfo);
      }}
    >
      <MyComponent />
    </ConfigurableErrorBoundary>
  );
}

第二部分:错误边界模式

2.1 粒度控制

javascript
// 粗粒度:整个应用
function CoarseGrained() {
  return (
    <ErrorBoundary fallback={<FullPageError />}>
      <App />
    </ErrorBoundary>
  );
}
// 优点:简单
// 缺点:一个错误导致整个应用不可用

// 细粒度:每个组件
function FineGrained() {
  return (
    <div>
      <ErrorBoundary fallback={<HeaderError />}>
        <Header />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<MainError />}>
        <Main />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
    </div>
  );
}
// 优点:隔离错误,其他部分正常
// 缺点:代码冗余

// 最佳实践:关键功能边界
function BestPractice() {
  return (
    <div>
      <Header />  {/* 不容易出错,不包裹 */}
      
      <ErrorBoundary fallback={<DashboardError />}>
        <Dashboard />  {/* 复杂组件,包裹 */}
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<WidgetError />}>
        <ThirdPartyWidget />  {/* 第三方组件,包裹 */}
      </ErrorBoundary>
      
      <Footer />  {/* 简单组件,不包裹 */}
    </div>
  );
}

2.2 嵌套错误边界

javascript
// 多层错误处理
function NestedErrorBoundaries() {
  return (
    <ErrorBoundary fallback={<AppLevelError />}>
      {/* 顶层:应用级错误 */}
      
      <Header />
      
      <ErrorBoundary fallback={<PageLevelError />}>
        {/* 第二层:页面级错误 */}
        
        <PageContent />
        
        <ErrorBoundary fallback={<ComponentError />}>
          {/* 第三层:组件级错误 */}
          <ComplexWidget />
        </ErrorBoundary>
      </ErrorBoundary>
      
      <Footer />
    </ErrorBoundary>
  );
}

// 执行逻辑:
// 1. ComplexWidget出错 -> ComponentError显示
// 2. PageContent出错 -> PageLevelError显示
// 3. Header/Footer出错 -> AppLevelError显示

// 优先级错误处理
class PriorityErrorBoundary extends React.Component {
  state = { error: null };
  
  static getDerivedStateFromError(error) {
    return { error };
  }
  
  componentDidCatch(error, errorInfo) {
    const priority = this.props.priority || 'low';
    
    if (priority === 'critical') {
      // 关键错误:立即通知
      alertCriticalError(error, errorInfo);
    } else if (priority === 'high') {
      // 高优先级:记录并通知
      logError(error, errorInfo);
      notifyTeam(error);
    } else {
      // 低优先级:仅记录
      logError(error, errorInfo);
    }
  }
  
  render() {
    if (this.state.error) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <div>
      <PriorityErrorBoundary 
        priority="critical" 
        fallback={<CriticalError />}
      >
        <PaymentSystem />
      </PriorityErrorBoundary>
      
      <PriorityErrorBoundary 
        priority="low" 
        fallback={<MinorError />}
      >
        <Comments />
      </PriorityErrorBoundary>
    </div>
  );
}

2.3 错误恢复

javascript
// 带重置功能的错误边界
class ResettableErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  reset = () => {
    this.setState({ hasError: false, error: null });
  };
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>出错了</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={this.reset}>重试</button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 自动重置
class AutoResetErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidUpdate(prevProps) {
    // Props变化时自动重置
    if (this.state.hasError && prevProps.resetKey !== this.props.resetKey) {
      this.setState({ hasError: false, error: null });
    }
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    
    return this.props.children;
  }
}

// 使用
function UserProfile({ userId }) {
  return (
    <AutoResetErrorBoundary resetKey={userId}>
      <ProfileContent userId={userId} />
    </AutoResetErrorBoundary>
  );
}
// userId变化时自动重置错误状态

// 带重试限制的错误边界
class RetryLimitErrorBoundary extends React.Component {
  state = { 
    hasError: false, 
    error: null,
    retryCount: 0
  };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  retry = () => {
    const { retryCount } = this.state;
    const maxRetries = this.props.maxRetries || 3;
    
    if (retryCount < maxRetries) {
      this.setState(prev => ({
        hasError: false,
        error: null,
        retryCount: prev.retryCount + 1
      }));
    }
  };
  
  render() {
    const { hasError, error, retryCount } = this.state;
    const maxRetries = this.props.maxRetries || 3;
    
    if (hasError) {
      if (retryCount >= maxRetries) {
        return (
          <div>
            <h2>多次重试失败</h2>
            <p>请刷新页面或联系支持</p>
          </div>
        );
      }
      
      return (
        <div>
          <h2>出错了</h2>
          <p>{error?.message}</p>
          <button onClick={this.retry}>
            重试 ({retryCount}/{maxRetries})
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

2.4 错误上报

javascript
// 错误上报服务集成
class ReportingErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // 上报到Sentry
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        contexts: {
          react: {
            componentStack: errorInfo.componentStack
          }
        }
      });
    }
    
    // 上报到自定义服务
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: error.toString(),
        componentStack: errorInfo.componentStack,
        url: window.location.href,
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString()
      })
    });
    
    // 上报到Google Analytics
    if (window.gtag) {
      window.gtag('event', 'exception', {
        description: error.toString(),
        fatal: true
      });
    }
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    
    return this.props.children;
  }
}

// 带用户上下文的错误上报
class ContextualErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    const { user, route } = this.props;
    
    reportError({
      error: error.toString(),
      componentStack: errorInfo.componentStack,
      user: {
        id: user?.id,
        email: user?.email
      },
      route: route,
      timestamp: Date.now()
    });
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    
    return this.props.children;
  }
}

// 使用
function App() {
  const user = useUser();
  const location = useLocation();
  
  return (
    <ContextualErrorBoundary 
      user={user}
      route={location.pathname}
      fallback={<ErrorPage />}
    >
      <AppContent />
    </ContextualErrorBoundary>
  );
}

第三部分:实战应用

3.1 路由级错误边界

javascript
// React Router错误边界
function AppRouter() {
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/"
          element={
            <ErrorBoundary fallback={<HomeError />}>
              <Home />
            </ErrorBoundary>
          }
        />
        
        <Route
          path="/dashboard"
          element={
            <ErrorBoundary fallback={<DashboardError />}>
              <Dashboard />
            </ErrorBoundary>
          }
        />
        
        <Route
          path="/profile"
          element={
            <ErrorBoundary fallback={<ProfileError />}>
              <Profile />
            </ErrorBoundary>
          }
        />
      </Routes>
    </BrowserRouter>
  );
}

// 统一路由错误处理
function RouterWithErrorBoundary() {
  return (
    <BrowserRouter>
      <ErrorBoundary 
        fallback={({ error, reset }) => (
          <RouteError error={error} onReset={reset} />
        )}
      >
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </ErrorBoundary>
    </BrowserRouter>
  );
}

3.2 异步组件错误边界

javascript
// lazy + ErrorBoundary
import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <ErrorBoundary fallback={<LoadError />}>
      <Suspense fallback={<Loading />}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// 处理chunk加载失败
class ChunkLoadErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    if (error.name === 'ChunkLoadError') {
      return { hasError: true, isChunkError: true };
    }
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      if (this.state.isChunkError) {
        return (
          <div>
            <h2>加载失败</h2>
            <p>请刷新页面重试</p>
            <button onClick={() => window.location.reload()}>
              刷新页面
            </button>
          </div>
        );
      }
      
      return <GenericError />;
    }
    
    return this.props.children;
  }
}

3.3 表单错误边界

javascript
// 表单提交错误处理
function FormWithErrorBoundary() {
  const [submitError, setSubmitError] = useState(null);
  
  const handleSubmit = async (data) => {
    try {
      setSubmitError(null);
      await submitForm(data);
    } catch (error) {
      setSubmitError(error);
    }
  };
  
  return (
    <ErrorBoundary fallback={<FormRenderError />}>
      {submitError && (
        <div className="submit-error">
          提交失败: {submitError.message}
        </div>
      )}
      
      <Form onSubmit={handleSubmit} />
    </ErrorBoundary>
  );
}
// ErrorBoundary捕获渲染错误
// try-catch捕获异步错误

3.4 第三方集成错误边界

javascript
// 包裹第三方组件
function ThirdPartyIntegration() {
  return (
    <ErrorBoundary
      fallback={<ThirdPartyFallback />}
      onError={(error) => {
        console.warn('Third party error:', error);
      }}
    >
      <ThirdPartyMap />
      <ThirdPartyChat />
      <ThirdPartyAnalytics />
    </ErrorBoundary>
  );
}

// 隔离不稳定组件
function IsolateUnstable() {
  return (
    <div>
      <StableHeader />
      
      <ErrorBoundary fallback={<WidgetPlaceholder />}>
        <UnstableWidget />
      </ErrorBoundary>
      
      <StableFooter />
    </div>
  );
}

注意事项

1. 只能是类组件

javascript
// ❌ 函数组件不能是错误边界
function FunctionErrorBoundary({ children }) {
  // 没有getDerivedStateFromError
  // 没有componentDidCatch
  return children;
}

// ✅ 必须是类组件
class ClassErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error(error, errorInfo);
  }
  
  render() {
    // ...
  }
}

// 函数组件的替代方案:使用库
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary fallback={<Error />}>
      <MyComponent />
    </ErrorBoundary>
  );
}

2. 不捕获自身错误

javascript
// ❌ 错误边界不能捕获自己的错误
class SelfErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    // 可以捕获子组件错误
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      throw new Error('Fallback error');  // ❌ 不能捕获
    }
    
    return this.props.children;
  }
}

// ✅ 使用外层错误边界
function SafeNesting() {
  return (
    <OuterErrorBoundary>
      <InnerErrorBoundary>
        <Content />
      </InnerErrorBoundary>
    </OuterErrorBoundary>
  );
}

3. 事件处理器需要try-catch

javascript
// 正确处理事件错误
function SafeComponent() {
  const [error, setError] = useState(null);
  
  const handleClick = () => {
    try {
      riskyOperation();
    } catch (err) {
      setError(err);
    }
  };
  
  if (error) {
    return <div>错误: {error.message}</div>;
  }
  
  return <button onClick={handleClick}>Click</button>;
}

常见问题

Q1: 错误边界和try-catch的区别?

A: 错误边界捕获渲染错误,try-catch捕获命令式代码错误。

Q2: 可以在函数组件中使用错误边界吗?

A: 不能创建,但可以使用类错误边界包裹函数组件。

Q3: 错误边界会影响性能吗?

A: 几乎无影响,只在错误发生时有开销。

Q4: 如何测试错误边界?

A: 手动抛出错误或使用测试库模拟错误。

Q5: 错误边界可以嵌套吗?

A: 可以,内层错误会被最近的边界捕获。

Q6: 异步错误能被捕获吗?

A: 不能,需要手动try-catch。

Q7: 开发环境和生产环境有区别吗?

A: 开发环境显示错误堆栈,生产环境显示fallback。

Q8: 错误边界能捕获Promise rejection吗?

A: 不能,需要.catch()或try-catch。

Q9: 如何区分不同类型的错误?

A: 在componentDidCatch中检查error对象属性。

Q10: 错误边界影响SEO吗?

A: 如果服务端渲染时出错,可能影响;客户端不影响。

总结

核心要点

1. 错误边界作用
   ✅ 捕获渲染错误
   ✅ 显示备用UI
   ✅ 防止应用崩溃
   ✅ 记录错误信息

2. 实现要点
   ✅ 必须是类组件
   ✅ getDerivedStateFromError
   ✅ componentDidCatch
   ✅ 合理的fallback

3. 使用场景
   ✅ 关键功能隔离
   ✅ 第三方组件包裹
   ✅ 路由级保护
   ✅ 异步组件保护

最佳实践

1. 粒度控制
   ✅ 关键功能单独包裹
   ✅ 避免过度嵌套
   ✅ 平衡隔离和复杂度

2. 用户体验
   ✅ 友好的错误提示
   ✅ 提供重试机制
   ✅ 保留部分功能
   ✅ 引导用户操作

3. 错误处理
   ✅ 上报错误
   ✅ 区分错误类型
   ✅ 添加上下文信息
   ✅ 监控错误趋势

错误边界是React应用健壮性的基石,合理使用能显著提升用户体验和应用稳定性。