Skip to content

深入理解 ErrorBoundary 与 Suspense 内部机制

throw 捕获链路(Error vs Promise)、错误边界处理、Suspense replay 机制、错误恢复策略

概述

ErrorBoundary 和 Suspense 在 React 内部共享同一套throw 捕获机制——区别在于 throw 出去的是 Error 还是 Promise(thenable)。理解这一底层统一性,就理解了 React 的异常处理架构。


一、统一的 throw 捕获链路

核心机制

javascript
// React 渲染时的 try-catch 框架(简化)
function renderRootSync(root, lanes) {
  workInProgress = root.current.alternate;

  while (workInProgress !== null) {
    try {
      // 正常的 beginWork → completeWork 循环
      workLoopSync();
    } catch (thrownValue) {
      // 捕获 throw 出来的值
      handleThrow(root, thrownValue);
    }
  }
}

function handleThrow(root, thrownValue) {
  if (isThenable(thrownValue)) {
    // throw 的是 Promise → Suspense 处理
    workInProgressSuspendedReason = SuspendedOnData;
    workInProgressThrownValue = thrownValue;
  } else {
    // throw 的是 Error → ErrorBoundary 处理
    workInProgressSuspendedReason = SuspendedOnError;
    workInProgressThrownValue = thrownValue;
  }
}

两条处理路径

组件渲染中 throw 了一个值

  ├── throw Error(错误对象)
  │     ↓
  │   向上查找最近的 ErrorBoundary(类组件 + getDerivedStateFromError)
  │     ↓
  │   找到 → 标记 ErrorBoundary 的 flags |= ShouldCapture
  │          → 创建 update 调用 getDerivedStateFromError(error)
  │          → 回退到 ErrorBoundary,重新 beginWork 渲染 fallback
  │     ↓
  │   没找到 → 整个应用崩溃(React 卸载整棵树)

  └── throw Promise / Thenable(异步数据未就绪)

      向上查找最近的 Suspense 边界

      找到 → 标记 Suspense 的 flags |= ShouldCapture
             → 显示 fallback
             → Promise resolve 后重新渲染子树

      没找到 → 未捕获的 Suspense,开发模式下报警告

二、ErrorBoundary 详解

定义(只能用类组件)

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

  // 静态方法:捕获错误,更新 state 以渲染 fallback UI
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // 实例方法:用于日志上报等副作用
  componentDidCatch(error, errorInfo) {
    console.error('捕获错误:', error);
    console.error('组件栈:', errorInfo.componentStack);
    reportError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <DefaultErrorUI error={this.state.error} />;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary fallback={<p>出错了</p>}>
  <RiskyComponent />
</ErrorBoundary>

ErrorBoundary 内部处理流程

javascript
// 当子组件 throw Error 时

// 1. handleThrow 捕获错误
function throwException(root, returnFiber, sourceFiber, value, renderLanes) {
  // 标记出错的 Fiber
  sourceFiber.flags |= Incomplete;

  // 2. 从出错的 Fiber 向上寻找 ErrorBoundary
  let workInProgress = returnFiber;
  while (workInProgress !== null) {
    if (workInProgress.tag === ClassComponent) {
      const ctor = workInProgress.type;
      if (typeof ctor.getDerivedStateFromError === 'function') {
        // 找到 ErrorBoundary!

        // 3. 创建一个 update,调用 getDerivedStateFromError
        const update = createClassErrorUpdate(workInProgress, value, renderLanes);
        // update.payload = () => getDerivedStateFromError(error)
        // → 这会更新 ErrorBoundary 的 state

        enqueueCapturedUpdate(workInProgress, update);

        // 4. 标记需要捕获
        workInProgress.flags |= ShouldCapture;
        workInProgress.lanes |= renderLanes;
        return;
      }
    }

    // 也检查 SuspenseComponent(Error 也可能被 Suspense 捕获用于重试)

    workInProgress = workInProgress.return;
  }

  // 没找到 ErrorBoundary → 标记 root 失败
  // → React 会卸载整棵树
}

Unwind 回退过程

javascript
// throw 后,React 需要从出错的节点"回退"到 ErrorBoundary

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;

  while (completedWork !== null) {
    if ((completedWork.flags & Incomplete) !== NoFlags) {
      // 这个节点或其子树出错了
      const next = unwindWork(completedWork);

      if (next !== null) {
        // unwindWork 返回了需要重新处理的 Fiber(ErrorBoundary 或 Suspense)
        // 清理 Incomplete 标记
        next.flags &= ~ShouldCapture;
        next.flags |= DidCapture; // 标记已捕获

        // 从这个节点重新开始 beginWork
        workInProgress = next;
        return;
      }
    }

    completedWork = completedWork.return;
  }
}

// unwindWork 做了什么:
function unwindWork(workInProgress) {
  switch (workInProgress.tag) {
    case ClassComponent: {
      if (workInProgress.flags & ShouldCapture) {
        return workInProgress; // 返回 ErrorBoundary,让它重新渲染
      }
      return null;
    }
    case SuspenseComponent: {
      if (workInProgress.flags & ShouldCapture) {
        return workInProgress; // 返回 Suspense,让它显示 fallback
      }
      return null;
    }
    default:
      return null;
  }
}

错误恢复

jsx
// ErrorBoundary 被重新 beginWork 后:
// getDerivedStateFromError 的返回值合并到 state
// → state.hasError = true
// → render() 返回 fallback UI
// → 正常渲染 fallback,不再渲染出错的子树

// 重置错误:
class ErrorBoundary extends React.Component {
  state = { hasError: false };

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

  resetError = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>出错了: {this.state.error?.message}</p>
          <button onClick={this.resetError}>重试</button>
        </div>
      );
    }
    return this.props.children;
  }
}

三、Suspense 详解

基础使用

jsx
import { Suspense, lazy, use } from 'react';

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

// use() 读取 Promise(React 19)
function UserProfile({ userPromise }) {
  const user = use(userPromise); // 如果 Promise 未 resolve → throw Promise
  return <div>{user.name}</div>;
}

// Suspense 边界
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <LazyComponent />
      <UserProfile userPromise={fetchUser()} />
    </Suspense>
  );
}

Suspense 内部处理流程

javascript
// 1. 子组件 throw thenable(Promise)

// use() 的简化实现
function use(thenable) {
  if (thenable.status === 'fulfilled') {
    return thenable.value; // 已 resolve → 直接返回
  }
  if (thenable.status === 'rejected') {
    throw thenable.reason; // 已 reject → throw Error
  }
  // pending → throw thenable
  throw thenable;
}

// 2. handleThrow 检测到是 thenable
// → workInProgressSuspendedReason = SuspendedOnData

// 3. throwException 向上查找 Suspense
function throwException(root, returnFiber, sourceFiber, value, renderLanes) {
  if (isThenable(value)) {
    let workInProgress = returnFiber;
    while (workInProgress !== null) {
      if (workInProgress.tag === SuspenseComponent) {
        // 找到 Suspense!

        // 注册 thenable 的 resolve 回调
        const wakeable = value;
        attachPingListener(root, wakeable, renderLanes);
        // → wakeable.then(() => { markRootPinged(root, lanes); })
        // → resolve 时重新调度渲染

        workInProgress.flags |= ShouldCapture;
        return;
      }
      workInProgress = workInProgress.return;
    }
  }
}

Suspense 的状态切换

javascript
// beginWork 中处理 SuspenseComponent
function updateSuspenseComponent(current, workInProgress) {
  const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;

  if (didSuspend) {
    // === 挂起状态 → 显示 fallback ===
    workInProgress.flags &= ~DidCapture;

    const fallback = workInProgress.pendingProps.fallback;
    const primaryChildFragment = workInProgress.child;

    // 隐藏主内容(设置 display: none 或不挂载)
    primaryChildFragment.flags |= Hidden;

    // 渲染 fallback
    return mountSuspenseFallbackChildren(workInProgress, fallback);
  } else {
    // === 正常状态 → 渲染 children ===
    return mountSuspensePrimaryChildren(workInProgress, children);
  }
}

Replay 机制

Suspense 的 "replay" 流程:

1. 首次渲染 → 子组件 throw Promise → 显示 fallback
2. Promise resolve → React 重新调度渲染
3. 再次渲染子组件 → 这次 use(promise) 返回值(status === 'fulfilled')
4. 正常渲染 → 隐藏 fallback,显示内容

如果再次 throw(数据还没好)→ 继续显示 fallback,等下一次 resolve

关键:React 不会"恢复"中断的渲染
      而是从 Suspense 边界开始完全重新渲染子树
      所以叫 "replay" 而非 "resume"

四、ErrorBoundary 与 Suspense 的关系

Suspense 中的错误处理

jsx
// Promise reject 时,Suspense 将其转换为 Error
// 需要外层 ErrorBoundary 捕获

<ErrorBoundary fallback={<p>加载失败</p>}>
  <Suspense fallback={<Spinner />}>
    <AsyncComponent />
  </Suspense>
</ErrorBoundary>

// 流程:
// AsyncComponent throw Promise → Suspense 显示 Spinner
// Promise reject → Suspense 内部 throw Error
// Error 冒泡到 ErrorBoundary → 显示"加载失败"

内部处理优先级

javascript
// throwException 的查找顺序
while (workInProgress !== null) {
  if (workInProgress.tag === SuspenseComponent && isThenable(value)) {
    // Suspense 优先处理 thenable
    handleSuspense(workInProgress, value);
    return;
  }

  if (workInProgress.tag === ClassComponent) {
    const ctor = workInProgress.type;
    if (typeof ctor.getDerivedStateFromError === 'function') {
      // ErrorBoundary 处理 Error
      handleErrorBoundary(workInProgress, value);
      return;
    }
  }

  workInProgress = workInProgress.return;
}

// 所以:
// throw Promise → 被最近的 Suspense 捕获
// throw Error → 被最近的 ErrorBoundary 捕获
// Suspense 不捕获 Error,ErrorBoundary 不捕获 Promise

五、ErrorBoundary 的局限

无法捕获的场景

jsx
// ❌ 事件处理器中的错误
function Button() {
  const handleClick = () => {
    throw new Error('点击出错'); // ErrorBoundary 捕获不到!
    // 事件处理器在 React 渲染流程之外,
    // 需要手动 try-catch
  };
  return <button onClick={handleClick}>点击</button>;
}

// ❌ 异步代码(setTimeout/Promise)
function Timer() {
  useEffect(() => {
    setTimeout(() => {
      throw new Error('定时器出错'); // 捕获不到!
    }, 1000);
  }, []);
}

// ❌ 服务端渲染(SSR)
// ErrorBoundary 的 componentDidCatch 在服务端不执行

// ❌ ErrorBoundary 自身的渲染错误
// ErrorBoundary 的 render() 或 getDerivedStateFromError() 出错 → 冒泡到上层

// ✅ 能捕获的:
// - 子组件的 render 中抛出的错误
// - useEffect / useLayoutEffect 中的错误(React 18+)
// - constructor / componentDidMount / componentDidUpdate 中的错误

事件处理器错误的解决方案

jsx
// 方案1:手动 try-catch
function Button() {
  const handleClick = () => {
    try {
      riskyOperation();
    } catch (error) {
      reportError(error);
      showToast('操作失败');
    }
  };
}

// 方案2:React 19 的全局错误处理
createRoot(container, {
  onCaughtError(error) {
    // ErrorBoundary 捕获的错误
    reportError(error);
  },
  onUncaughtError(error) {
    // 未被捕获的渲染错误
    showErrorPage(error);
  },
  onRecoverableError(error) {
    // 可恢复的错误(如 hydration mismatch)
    console.warn(error);
  },
});

六、嵌套策略

jsx
function App() {
  return (
    // 顶层:全局错误兜底
    <ErrorBoundary fallback={<CrashPage />}>
      <Layout>
        {/* 页面级:错误不影响其他页面 */}
        <ErrorBoundary fallback={<PageError />}>
          <Suspense fallback={<PageSkeleton />}>
            <DashboardPage />
          </Suspense>
        </ErrorBoundary>

        {/* 组件级:细粒度控制 */}
        <ErrorBoundary fallback={<WidgetError />}>
          <Suspense fallback={<WidgetSkeleton />}>
            <RealtimeWidget />
          </Suspense>
        </ErrorBoundary>
      </Layout>
    </ErrorBoundary>
  );
}

面试高频题

Q: ErrorBoundary 和 Suspense 在内部机制上有什么共同点?

它们共享同一套 throw 捕获机制。当组件 throw 时,React 都会从出错节点向上遍历 Fiber 树寻找边界。区别在于:throw Error → 被 ErrorBoundary(getDerivedStateFromError)捕获;throw Promise → 被 Suspense 捕获。找到后都标记 ShouldCapture,通过 unwindWork 回退到边界节点重新渲染。

Q: ErrorBoundary 为什么只能用类组件?

因为错误捕获依赖 getDerivedStateFromError(静态方法)和 componentDidCatch(生命周期),这些是类组件 API。函数组件没有对应的 Hook。React 团队表示未来可能提供 use(promise) 类似的错误处理 Hook,但目前(React 19)仍需类组件。

Q: Suspense 的 "replay" 是什么意思?

子组件 throw Promise 后,React 显示 fallback 并等待 Promise resolve。resolve 后 React 不会"恢复"中断的渲染,而是从 Suspense 边界完全重新渲染整棵子树。这次渲染中 use(promise) 直接返回值(因为 Promise 已 fulfilled),正常完成渲染。这个"重新开始"的过程叫 replay。

Q: ErrorBoundary 能捕获事件处理器中的错误吗?

不能。ErrorBoundary 只捕获 React 渲染流程(render、lifecycle、Effect)中的错误。事件处理器在 React 渲染管线之外执行,throw 的错误不会经过 React 的 try-catch。需要在事件处理器中手动 try-catch,或使用 React 19 的 createRoot({ onUncaughtError }) 全局处理。