深入理解 ErrorBoundary 与 Suspense 内部机制
throw 捕获链路(Error vs Promise)、错误边界处理、Suspense replay 机制、错误恢复策略
概述
ErrorBoundary 和 Suspense 在 React 内部共享同一套throw 捕获机制——区别在于 throw 出去的是 Error 还是 Promise(thenable)。理解这一底层统一性,就理解了 React 的异常处理架构。
一、统一的 throw 捕获链路
核心机制
// 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 详解
定义(只能用类组件)
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 内部处理流程
// 当子组件 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 回退过程
// 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;
}
}错误恢复
// 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 详解
基础使用
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 内部处理流程
// 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 的状态切换
// 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 中的错误处理
// 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 → 显示"加载失败"内部处理优先级
// 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 的局限
无法捕获的场景
// ❌ 事件处理器中的错误
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 中的错误事件处理器错误的解决方案
// 方案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);
},
});六、嵌套策略
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 }) 全局处理。