第 21 章

错误边界与错误处理体系

第21章:错误边界与错误处理体系

React 应用中未捕获的错误会让整棵组件树崩溃——这不是 bug,是逼迫开发者主动处理错误的设计哲学。

本章核心问题:错误边界只能捕获什么类型的错误?完整的错误处理分层防御如何构建? 读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

react-error-boundary:生产级封装

自己实现错误边界需要处理很多细节(重置状态、重试逻辑、key 重置技巧)。react-error-boundary 库提供了完善的实现:

npm install react-error-boundary

基本用法

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

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

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => {
        // 上报到 Sentry 等监控服务
        reportError(error, info);
      }}
      onReset={() => {
        // 重置应用状态(如清空缓存、重置 store)
      }}
    >
      <UserDashboard />
    </ErrorBoundary>
  );
}

useErrorBoundary Hook

react-error-boundary 提供了 useErrorBoundary Hook,让函数组件能够主动触发最近父级错误边界:

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

function DataFetcher({ userId }: { userId: string }) {
  const { showBoundary } = useErrorBoundary();

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch((error) => {
        // 将错误抛给最近的 ErrorBoundary
        showBoundary(error);
      });
  }, [userId]);

  // ...
}

这解决了一个重要问题:错误边界只能捕获渲染阶段的错误,而 useEffect 中的异步错误默认不会被捕获。showBoundary 让你手动将异步错误路由到错误边界。

重置错误边界

当错误是临时性的(如网络抖动),用户点击重试应该清除错误状态并重新渲染。resetKeys 是一种优雅的重置方式:

function App() {
  const [location, setLocation] = useState(window.location.pathname);

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      resetKeys={[location]} // location 变化时自动重置错误边界
      onReset={() => setLocation(window.location.pathname)}
    >
      <Router />
    </ErrorBoundary>
  );
}

Level 2 · 它是怎么运行的(3-5年经验)

Error Boundary 类组件实现

错误边界必须是类组件,因为它依赖两个类组件生命周期方法,函数组件目前还没有对应的 Hook(截至 React 19,这仍是事实)。

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

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

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

  // 静态方法:从错误中派生新状态。渲染阶段调用,必须是纯函数
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  // 实例方法:错误已确认后的副作用(日志、上报)。提交阶段调用
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError && this.state.error) {
      const { fallback } = this.props;
      if (typeof fallback === 'function') {
        return fallback(this.state.error);
      }
      return fallback ?? <div>出现了一些错误</div>;
    }
    return this.props.children;
  }
}

getDerivedStateFromError vs componentDidCatch 的分工


Level 3 · 规范怎么定义的(资深)

本章涉及的 API 规范和设计决策已融入各层级的讨论中。


Level 4 · 边界与陷阱(所有人)

为什么未处理的错误会让整棵树崩溃

React 在渲染时,每个组件的 render 函数(或函数组件体)都是同步执行的。如果其中一个抛出异常,React 不知道如何继续渲染该组件及其子节点——它们处于不确定状态。在 React 16 之前,React 会保留崩溃时的 UI(通常是损坏状态),这比白屏更危险,因为用户可能在损坏状态下继续操作,产生错误数据。

React 16 引入错误边界,并明确了规则:渲染阶段的未捕获错误会卸载整棵 React 树。这逼迫开发者主动处理错误,而不是寄希望于损坏的 UI 继续工作。

错误边界的放置策略

错误边界不是越多越好,也不是一个全局的就够了。正确的放置策略需要权衡:

粒度

App
├── GlobalErrorBoundary     ← 最外层:捕获所有漏网之鱼,防白屏
│   ├── Header              ← Header 不需要边界(导航崩溃整个 App 都没用了)
│   ├── Sidebar
│   │   └── SidebarErrorBoundary  ← 独立功能区:侧边栏崩溃不影响主内容
│   └── Main
│       ├── RouteErrorBoundary    ← 路由级别:每个页面独立隔离
│       │   └── PageContent
│       │       ├── CardErrorBoundary  ← 卡片级别:Dashboard 的数据卡片
│       │       └── ChartErrorBoundary ← 图表独立边界

原则

错误上报:集成 Sentry

npm install @sentry/react
// src/main.tsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration(),
  ],
  tracesSampleRate: 0.1,      // 采样 10% 的性能追踪
  replaysOnErrorSampleRate: 1, // 错误时 100% 记录回放
});

// 使用 Sentry 包装的 ErrorBoundary(自动上报)
import { ErrorBoundary } from '@sentry/react';

function App() {
  return (
    <ErrorBoundary
      fallback={<ErrorPage />}
      showDialog // 在 Sentry 上显示用户反馈对话框
    >
      <Router />
    </ErrorBoundary>
  );
}

在自定义 componentDidCatch 中手动上报:

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  Sentry.withScope((scope) => {
    scope.setExtra('componentStack', errorInfo.componentStack);
    scope.setTag('component', 'ErrorBoundary');
    Sentry.captureException(error);
  });
}

React 19 的错误处理改进

React 19 在错误处理机制上做了显著改进。

新的根选项:onCaughtError 与 onUncaughtError

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root')!, {
  // 被错误边界捕获的错误
  onCaughtError(error, errorInfo) {
    console.log('被错误边界捕获:', error.message);
    // errorInfo.componentStack 包含组件堆栈
  },

  // 未被任何错误边界捕获的错误(将导致整棵树卸载)
  onUncaughtError(error, errorInfo) {
    console.error('未捕获的错误,应用将崩溃:', error.message);
    Sentry.captureException(error);
  },

  // 可恢复的错误(如 hydration 不匹配,React 会重新渲染修复)
  onRecoverableError(error, errorInfo) {
    console.warn('可恢复错误:', error.message);
  },
});

这些回调相比之前的 window.onerrorwindow.addEventListener('error') 更精准,因为它们能区分错误的来源和严重程度。

更清晰的 Hydration 错误信息

React 19 大幅改进了 SSR hydration 错误的报错信息。之前的错误只说"hydration mismatch",React 19 会精确指出服务端和客户端渲染的内容差异:

Hydration failed because the server rendered HTML didn't match the client.
  Server: <div class="theme-dark">
  Client: <div class="theme-light">

这让调试 SSR 应用的类型错误变得容易得多。

异步错误处理

错误边界只能捕获渲染阶段(组件函数体、生命周期)的同步错误。以下场景需要单独处理:

事件处理器中的错误

function DeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
  const [error, setError] = useState<Error | null>(null);
  const { showBoundary } = useErrorBoundary();

  async function handleDelete() {
    try {
      await onDelete();
    } catch (e) {
      const error = e instanceof Error ? e : new Error(String(e));

      if (error.name === 'NetworkError') {
        // 可恢复错误:在组件内部显示提示
        setError(error);
      } else {
        // 不可恢复错误:上抛到错误边界
        showBoundary(error);
      }
    }
  }

  return (
    <>
      {error && <p role="alert" className="error">{error.message}</p>}
      <button onClick={handleDelete}>删除</button>
    </>
  );
}

数据获取中的错误处理

使用 React Query 时,错误处理有完善的机制:

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    retry: 3,              // 失败后自动重试 3 次
    retryDelay: 1000,      // 每次重试间隔 1 秒
  });

  if (isLoading) return <Skeleton />;

  if (error) {
    return (
      <div role="alert">
        <p>加载用户信息失败:{error.message}</p>
        <button onClick={() => refetch()}>重试</button>
      </div>
    );
  }

  return <UserCard user={data} />;
}

一个完整的错误处理体系不是某个单一技术的问题,而是分层防御:渲染错误靠错误边界,异步错误靠 try/catch 和状态管理,全局兜底靠监控上报。每一层都有明确的职责,组合在一起才能让应用在面对各种失败模式时优雅降级,而不是让用户盯着白屏无从下手。

本章评分
4.7  / 5  (8 评分)

💬 留言讨论