陌上人如玉
公子世无双

8.3 Error Boundary

8.3 Error Boundary

一句话: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>

九、面试延伸

  1. 「什么是 Error Boundary?」
    Error Boundary 是 React 的「组件级错误捕获机制」——它捕获子树在渲染、生命周期、构造函数中抛出的错误,然后渲染一个「降级 UI」(fallback),而不是让整个应用崩溃(白屏)。它只能用类组件实现,依赖两个方法:static getDerivedStateFromError(error)(返回新 state → 渲染降级 UI)与 componentDidCatch(error, info)(提交阶段回调 → 上报错误)。

  2. 「为什么函数组件不能实现 Error Boundary?」
    因为 Error Boundary 的核心能力是「在渲染失败时替换渲染结果」——这需要一个「有状态、可在渲染失败时被 React 调用」的实体。而 Hooks(useState/useEffect)都在「渲染成功之后」才运行,无法介入「渲染抛错」这个时机。所以 React 规定「Error Boundary 必须是类组件」(但可以用 react-error-boundary 这类库包装,让使用方是函数组件)。

  3. 「Error Boundary 能捕获哪些错误、不能捕获哪些?」
    能捕获:子组件在「渲染、生命周期方法、构造函数」中抛出的错误(包括后代组件)。不能捕获:① 事件处理函数里的错误(要 try/catch);② 异步代码(setTimeout、Promise、async 函数);③ 服务端渲染中的错误;④ Error Boundary 自身抛出的错误(需要外层再包);⑤ useEffect 里抛出的错误。所以「渲染错误用 Error Boundary,其它错误用全局监听/try-catch」。

  4. 「getDerivedStateFromError 和 componentDidCatch 有什么区别?」
    ① 时机——getDerivedStateFromError 在渲染阶段被调用(必须是静态纯函数,不能有副作用),用于「返回新的 state 来渲染降级 UI」;componentDidCatch 在提交阶段被调用(可以有副作用),用于「上报错误、打日志」。② 用途——前者负责「UI 降级」,后者负责「可观测性」。③ 两者可以只用其一(但通常两个都实现:一个降级、一个上报)。

  5. 「Error Boundary 应该放在哪里?」
    建议分层放置,粒度按「用户可感知的区域」划分:① 应用级(最外层,最后兜底,显示「整个页面出错了」+ 刷新/返回);② 区域级(每个独立功能块,如「仪表盘」「推荐列表」,一个区域崩了不影响其它区域);③ 第三方组件级(图表、富文本、地图——它们可能因异常数据崩溃);④ 路由级(每个页面独立兜底,数据路由下用 errorElement 更简洁)。只放一个最外层的问题是「任何局部错误都会让整页崩掉」。

  6. 「Error Boundary 能恢复应用状态吗?」
    不能——它只做「替换渲染」。错误发生后,出错的组件子树已经被卸载,其内部状态丢失;而「其它地方的状态」(如全局 store、Query 缓存)可能已经处于不一致的状态。所以「重试」时需要主动做「状态重置」:① 重挂载出错的子树(通过改变 key);② 清理相关缓存(queryClient.resetQueries());③ 重置相关的全局状态。react-error-boundary 的 onReset + resetKeys 就是为此设计的。

  7. 「为什么事件处理里的错误不会被捕获?」
    因为事件处理函数不在 React 的渲染过程中执行——它是在「用户点击」时由浏览器事件系统调用的,此时 React 的渲染早已完成。Error Boundary 的机制是「在渲染子树时捕获抛出的错误」,而事件回调的调用栈与渲染无关,所以捕获不到。同理 setTimeout / Promise 的回调也「脱离了渲染上下文」。这些错误需要「组件内 try/catch」或「全局 window.onerror / unhandledrejection 监听」。

  8. 「生产环境的 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 对排查很有价值)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 8.3 Error Boundary

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫