
一句话:Error Boundary 是 React 的「组件级错误边界」——它捕获子树在渲染过程中抛出的错误,显示降级 UI 而不是让整个应用白屏;只能用类组件实现(因为需要
componentDidCatch)。
一、为什么需要它
// ❌ 没有 Error Boundary 时:一个组件抛错 → 整个 React 树卸载 → 白屏
function BadComponent({ data }: { data: Data }) {
return <div>{data.user.name}</div>; // ⚠️ data.user 为 undefined 时抛错
}
function App() {
return (
<div>
<Header /> {/* 也会跟着消失 */}
<BadComponent data={data} />
<Footer />
</div>
);
}
// 结果:React 18 之前会「卸载整棵树」→ 用户看到白屏(且没有错误提示)
// React 18+ 仍然会「尽力渲染」但没有 Error Boundary 时行为不可靠
⭐ 核心认知:
React 的设计是「渲染错误无法被 try/catch 捕获」——
因为渲染是「异步、可中断、可重放」的(见 4.3),
组件函数可能在不同的时机被调用。
所以 React 提供了「Error Boundary」这个专门机制。
二、只能用类组件实现
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, info: ErrorInfo) => void;
}
interface State {
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { error: null };
// ① 渲染阶段捕获:返回「新的 state」(用来渲染降级 UI)
static getDerivedStateFromError(error: Error): State {
return { error };
}
// ② 提交阶段回调:用于「上报错误」(可以有副作用)
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('ErrorBoundary 捕获:', error, info.componentStack);
this.props.onError?.(error, info);
reportError(error, { componentStack: info.componentStack });
}
// ③ 提供「重置」能力(否则降级 UI 会一直显示)
reset = () => this.setState({ error: null });
render() {
const { error } = this.state;
if (error) {
const { fallback } = this.props;
return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div className="error-page">
<h2>页面出错了</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
<button onClick={() => (window.location.href = '/')}>返回首页</button>
</div>
)}
onError={(error, info) => trackError(error, info)}
>
<Page />
</ErrorBoundary>
);
}
⚠️ 为什么函数组件不能实现 Error Boundary?
因为它的核心能力是「在渲染失败时【替换渲染结果】」——
这需要一个「有状态的、生命周期可介入的」实体,
而 Hooks(useState/useEffect)都在「渲染成功之后」才运行,
【无法介入渲染失败】这个过程。
→ 所以严格来说,Error Boundary 必须是「类组件」
(但你可以引入 `react-error-boundary` 这类库,用函数组件作为使用方)
三、两个生命周期方法的分工
| 方法 | 时机 | 用途 |
|---|---|---|
getDerivedStateFromError(error) |
渲染阶段(不能有副作用) | 返回新 state → 渲染降级 UI |
componentDidCatch(error, info) |
提交阶段(可以有副作用) | 上报错误、打日志 |
⚠️ 两个关键的约束:
① `getDerivedStateFromError` 必须是【静态方法】且【纯函数】(不能调 API)
② 只有 `componentDidCatch` 里才能做副作用(上报)
③ 两个方法可以只用其中一个(但通常两个都要)
四、能捕获什么
✅ 捕获以下位置抛出的错误:
① 子组件【渲染】过程中的错误
· 组件函数体里 throw
· 组件函数体里对 undefined/null 取属性
· `useMemo`/`useState` 初始化函数里 throw(注意:见下面的坑)
② 子组件【生命周期方法】中的错误(类组件)
③ 子组件【构造函数】中的错误
④ 子组件树的「所有后代」(只要在 ErrorBoundary 的子树里)
五、捕获不到什么(重要)
❌ 捕获不到:
① 事件处理函数里的错误
<button onClick={() => { throw new Error('x'); }} />
→ 需要 try/catch(或全局监听)
② 异步代码里的错误
setTimeout(() => { throw new Error('x'); }, 0);
Promise.reject(new Error('x'));
async 函数里 throw
→ 需要 catch / unhandledrejection 监听
③ 服务端渲染(SSR)中的错误
→ 由服务端的错误处理负责
④ Error Boundary 自身抛出的错误
→ 需要「外层再包一个」Error Boundary
⑤ 事件回调中、useEffect 里的错误
⑥ React 之外的 DOM 操作错误(如手动 appendChild 失败)
⑦ 部分 React 内部错误(如某些并发相关的不可恢复错误)
// ❌ 事件处理里的错误不会被 ErrorBoundary 捕获
function Button() {
const handleClick = () => {
throw new Error('点击出错'); // ⚠️ ErrorBoundary 捕获不到
};
return <button onClick={handleClick}>点我</button>;
}
// ✅ 需要自己 try/catch
function Button2() {
const handleClick = () => {
try {
riskyOperation();
} catch (err) {
setError(err); // 或 toast / 上报
}
};
return <button onClick={handleClick}>点我</button>;
}
// ✅ useEffect 里的错误也不会被捕获(React 18 之前会,现在不会)
useEffect(() => {
fetchData().then(() => {
throw new Error('异步错误'); // ⚠️ 捕获不到
});
}, []);
// ✅ 用 catch + state
useEffect(() => {
fetchData().then(setData).catch(setError);
}, []);
六、分层放置策略
// ✅ 推荐:分层放置(粒度按「区域」)
function App() {
return (
<ErrorBoundary fallback={<AppCrash />}> {/* ① 应用级(最后兜底) */}
<TopNav />
<main>
<ErrorBoundary fallback={<WidgetError />}> {/* ② 区域级 */}
<Dashboard />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}> {/* ③ 另一个独立区域 */}
<RecommendList />
</ErrorBoundary>
</main>
<Footer />
</ErrorBoundary>
);
}
分层的三个原则:
① 【粒度】按「用户可感知的区域」划分——一个区域崩了不该影响其它区域
② 【不重复】只在下「可能出错且希望隔离」的子树外包
③ 【有降级 UI】fallback 要「说明 + 出路」(重试/返回),而不是一行红字
⚠️ 只放一个最外层的 Error Boundary 的问题:
→ 任何局部错误都会让「整页变成错误页」(用户失去整个界面)
// ✅ 常见的「局部隔离」场景
// ① 第三方组件(可能因数据异常崩溃)
<ErrorBoundary fallback={<div>图表加载失败</div>}>
<ThirdPartyChart data={data} />
</ErrorBoundary>
// ② 富文本 / Markdown 渲染(用户输入的内容可能不合法)
<ErrorBoundary fallback={<pre>{rawContent}</pre>}>
<RichTextRenderer content={rawContent} />
</ErrorBoundary>
// ③ 路由级(每个页面独立兜底)
<Routes>
<Route path="/a" element={<ErrorBoundary fallback={<PageError />}><PageA /></ErrorBoundary>} />
<Route path="/b" element={<ErrorBoundary fallback={<PageError />}><PageB /></ErrorBoundary>} />
</Routes>
// (数据路由下用 errorElement 更简洁,见 6.9)
七、react-error-boundary 库
// ✅ 用库可以避免手写类组件,并提供更完整的 API
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 重置时清理状态(如刷新数据)
queryClient.clear();
}}
onError={(error, info) => reportError(error, info)}
resetKeys={[userId]} // ⭐ 这些 key 变化时「自动重置」
>
<Page userId={userId} />
</ErrorBoundary>
);
}
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<h2>出错了</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
// ✅ 它还能让「函数组件」主动把错误抛给最近的 Error Boundary
function SubmitButton() {
const { showBoundary } = useErrorBoundary();
const handleClick = async () => {
try {
await submit();
} catch (err) {
showBoundary(err); // ⭐ 主动上抛(事件处理里的错误也能走边界)
}
};
return <button onClick={handleClick}>提交</button>;
}
| 特性 | 手写 | react-error-boundary |
|---|---|---|
| 使用方式 | 类组件包裹 | 函数组件 + props |
resetKeys |
自己实现 | ✅ |
useErrorBoundary |
没有 | ✅(事件里主动上抛) |
onReset |
自己实现 | ✅ |
| 体积 | 0 | 很小(约 2KB) |
八、常见坑
// ① 以为 Error Boundary 能捕获「事件处理里的错误」
<button onClick={() => { throw new Error(); }} /> // ❌ 捕获不到
// ② 以为能捕获「异步/Promise 的错误」
Promise.reject(new Error()); // ❌
// ③ 只用了 getDerivedStateFromError 没实现「重置」
// → 用户看到降级 UI 后只能刷新页面
// ④ fallback 只是一行红字(没有出路)
fallback={<div>出错了</div>} // ⚠️ 应该给「重试/返回」
// ⑤ 只放一个最外层的 Error Boundary(局部错误导致整页崩)
// ⑥ 在 Error Boundary 的 fallback 里「再次抛错」→ 需要外层再包一个
// ⑦ 用 try/catch 包 JSX(语法上行不通)
try { return <Comp />; } catch {} // ❌ 捕获不到 JSX 渲染错误
// ⑧ 把 Error Boundary 放在「会被卸载的地方」(如 Modal 内部)→ 起不到兜底作用
// ⑨ 认为「React 18 会用 try/catch 替代 Error Boundary」(没有)
// ⑩ 在生产环境「把错误信息(含堆栈)直接显示给用户」(可能泄露内部实现)
fallback={<pre>{error.stack}</pre>} // ⚠️ 生产环境应隐藏
// ⑪ 忘记「上报错误」(Error Boundary 的价值一半在「可观测性」)
// ⑫ 在 StrictMode 下「错误被捕获两次」(双调用导致)→ 上报要幂等/去重
// ⑬ 认为「Error Boundary 能恢复应用状态」→ 它只「替换渲染」,状态需要自己清
// ⑭ 用 Error Boundary 代替「输入校验」(应该提前校验而不是靠崩溃兜底)
// ⑮ 在 useEffect 里 throw(不会被捕获)→ 用 state 表达错误
useEffect(() => {
throw new Error('x'); // ❌ 捕获不到
}, []);
// ⑥ 的说明:fallback 里抛错会「冒泡到外层 Error Boundary」
<ErrorBoundary fallback={<div>内层兜底</div>}>
<ErrorBoundary fallback={<ThrowsError />}> {/* ⚠️ 需要外层再兜一层 */}
<Page />
</ErrorBoundary>
</ErrorBoundary>
九、面试延伸
- 「什么是 Error Boundary?」
Error Boundary 是 React 的「组件级错误捕获机制」——它捕获子树在渲染、生命周期、构造函数中抛出的错误,然后渲染一个「降级 UI」(fallback),而不是让整个应用崩溃(白屏)。它只能用类组件实现,依赖两个方法:static getDerivedStateFromError(error)(返回新 state → 渲染降级 UI)与componentDidCatch(error, info)(提交阶段回调 → 上报错误)。 -
「为什么函数组件不能实现 Error Boundary?」
因为 Error Boundary 的核心能力是「在渲染失败时替换渲染结果」——这需要一个「有状态、可在渲染失败时被 React 调用」的实体。而 Hooks(useState/useEffect)都在「渲染成功之后」才运行,无法介入「渲染抛错」这个时机。所以 React 规定「Error Boundary 必须是类组件」(但可以用react-error-boundary这类库包装,让使用方是函数组件)。 -
「Error Boundary 能捕获哪些错误、不能捕获哪些?」
能捕获:子组件在「渲染、生命周期方法、构造函数」中抛出的错误(包括后代组件)。不能捕获:① 事件处理函数里的错误(要try/catch);② 异步代码(setTimeout、Promise、async函数);③ 服务端渲染中的错误;④ Error Boundary 自身抛出的错误(需要外层再包);⑤useEffect里抛出的错误。所以「渲染错误用 Error Boundary,其它错误用全局监听/try-catch」。 -
「
getDerivedStateFromError和componentDidCatch有什么区别?」
① 时机——getDerivedStateFromError在渲染阶段被调用(必须是静态纯函数,不能有副作用),用于「返回新的 state 来渲染降级 UI」;componentDidCatch在提交阶段被调用(可以有副作用),用于「上报错误、打日志」。② 用途——前者负责「UI 降级」,后者负责「可观测性」。③ 两者可以只用其一(但通常两个都实现:一个降级、一个上报)。 -
「Error Boundary 应该放在哪里?」
建议分层放置,粒度按「用户可感知的区域」划分:① 应用级(最外层,最后兜底,显示「整个页面出错了」+ 刷新/返回);② 区域级(每个独立功能块,如「仪表盘」「推荐列表」,一个区域崩了不影响其它区域);③ 第三方组件级(图表、富文本、地图——它们可能因异常数据崩溃);④ 路由级(每个页面独立兜底,数据路由下用errorElement更简洁)。只放一个最外层的问题是「任何局部错误都会让整页崩掉」。 -
「Error Boundary 能恢复应用状态吗?」
不能——它只做「替换渲染」。错误发生后,出错的组件子树已经被卸载,其内部状态丢失;而「其它地方的状态」(如全局 store、Query 缓存)可能已经处于不一致的状态。所以「重试」时需要主动做「状态重置」:① 重挂载出错的子树(通过改变key);② 清理相关缓存(queryClient.resetQueries());③ 重置相关的全局状态。react-error-boundary的onReset+resetKeys就是为此设计的。 -
「为什么事件处理里的错误不会被捕获?」
因为事件处理函数不在 React 的渲染过程中执行——它是在「用户点击」时由浏览器事件系统调用的,此时 React 的渲染早已完成。Error Boundary 的机制是「在渲染子树时捕获抛出的错误」,而事件回调的调用栈与渲染无关,所以捕获不到。同理setTimeout/ Promise 的回调也「脱离了渲染上下文」。这些错误需要「组件内 try/catch」或「全局window.onerror/unhandledrejection监听」。 -
「生产环境的 fallback 应该注意什么?」
三点:① 不要暴露内部细节——不要显示error.stack、接口路径、内部错误码(可能泄露实现细节或被利用);给用户「友好的说明 + 出路」(重试、返回首页、联系客服),详细堆栈上报到监控平台;② 要有「出路」——只有一行「出错了」的降级 UI 等于把用户困住;③ 区分错误类型——「网络错误」「权限问题」「数据异常」的文案与出路不同(可结合error.name/error.message判断)。此外要在componentDidCatch里上报错误(带componentStack),这是排查线上问题的关键。
一句话速记
Error Boundary = 「渲染错误的组件级捕获」,只能用类组件实现(
static getDerivedStateFromError渲染降级 UI +componentDidCatch上报,前者纯函数、后者可副作用);只能捕获「渲染 / 生命周期 / 构造函数」的错误,捕获不到「事件处理、异步(Promise/setTimeout)、SSR、自身、useEffect」的错(要用try/catch+ 全局监听);要分层放置(应用级 + 区域级 + 第三方组件级),只放一层会让局部错误变整页崩;必须有「重置/重试」(react-error-boundary的resetKeys/useErrorBoundary很实用);生产环境别暴露堆栈,但要上报(componentStack对排查很有价值)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!