本文 AI 產出,尚未審核

TypeScript

單元:錯誤處理與例外(Error Handling)

主題:Error Boundary in TS + React


簡介

React 應用程式中,未捕捉的例外會導致整個 UI 樹崩潰,使用者只能看到一個空白畫面或是瀏覽器的錯誤訊息。對於 單頁應用(SPA) 來說,這種情況極不友好,也會影響品牌形象與使用者留存率。

Error Boundary(錯誤邊界) 是 React 官方提供的一種機制,允許開發者在 元件層級 捕捉渲染、生命週期以及子樹的錯誤,並以安全的方式呈現備援 UI。結合 TypeScript,我們不只可以在編譯階段獲得型別保護,還能為錯誤資訊、props、state 定義嚴謹的介面,提升程式碼可讀性與維護性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你建立 可重用、型別安全 的錯誤邊界,並展示在真實專案中如何與日誌服務、重置機制等結合。


核心概念

1. 為什麼需要 Error Boundary

  • 渲染錯誤不會傳遞到全域:React 只會把錯誤往上拋到最近的錯誤邊界,未捕捉的例外會導致整棵元件樹卸載。
  • 提升使用者體驗:錯誤邊界可以顯示「抱歉,系統發生問題」的備援 UI,讓使用者仍能操作其他功能。
  • 集中管理錯誤:可以在單一位置統一上傳錯誤、顯示提示或執行清理工作。

2. Error Boundary 的兩個關鍵生命週期

方法 目的 在 TypeScript 中的型別
static getDerivedStateFromError(error: Error) 在渲染錯誤發生後,根據錯誤更新 state(通常是 hasError: true error: Error
componentDidCatch(error: Error, info: React.ErrorInfo) 實際上「捕捉」錯誤,可在此上傳錯誤、寫日誌等 info: React.ErrorInfo

注意:只有 class component 才能直接實作上述兩個靜態方法;若想在函式元件使用,需借助第三方庫或自訂 HOC。

3. 使用 TypeScript 定義錯誤邊界的型別

interface ErrorBoundaryProps {
  /** 要顯示的備援 UI,若未提供則使用預設 */
  fallback?: React.ReactNode;
  /** 子元件 */
  children: React.ReactNode;
}

interface ErrorBoundaryState {
  /** 是否發生錯誤 */
  hasError: boolean;
  /** 捕捉到的錯誤物件,可用於顯示或上傳 */
  error?: Error;
}

4. 基本實作:Class Component

import React from "react";

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

  /** 在錯誤發生時把 hasError 設為 true */
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return { hasError: true, error };
  }

  /** 可以在此把錯誤上傳至後端或第三方服務 */
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error("ErrorBoundary 捕捉到錯誤:", error, info);
    // 例:上傳到 Sentry
    // Sentry.captureException(error, { extra: info });
  }

  render() {
    if (this.state.hasError) {
      // 使用者自訂的 fallback,或預設訊息
      return this.props.fallback ?? (
        <div style={{ padding: "2rem", textAlign: "center" }}>
          <h2>⚠️ 發生錯誤</h2>
          <p>抱歉,系統暫時無法顯示此區塊。</p>
        </div>
      );
    }
    return this.props.children;
  }
}

重點getDerivedStateFromError 必須回傳 部分 state(Partial),而非完整的 ErrorBoundaryState;這是 React 的規範。

5. 函式元件版:使用 react-error-boundary

import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary";

function FallbackComponent({ error, resetErrorBoundary }: {
  error: Error;
  resetErrorBoundary: () => void;
}) {
  return (
    <div role="alert" style={{ padding: "1.5rem", background: "#ffecec" }}>
      <p>❌ <strong>{error.message}</strong></p>
      <button onClick={resetErrorBoundary}>重新載入</button>
    </div>
  );
}

/** 包裝任何子元件的函式版錯誤邊界 */
export const MyErrorBoundary: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => (
  <ReactErrorBoundary
    FallbackComponent={FallbackComponent}
    onError={(error, info) => {
      console.warn("函式版捕捉到錯誤:", error, info);
      // 上傳至 LogRocket、Sentry…
    }}
    onReset={() => {
      // 例如清除快取、重設全域狀態等
    }}
  >
    {children}
  </ReactErrorBoundary>
);
  • react-error-boundary 內部已經實作了 class component 的兩個生命週期,讓函式元件也能享受同樣的功能。
  • resetErrorBoundary 讓使用者可以在 UI 上手動「重置」錯誤狀態。

6. 高階組件(HOC)封裝

如果想在 多個頁面 重複使用錯誤邊界,可以寫一個 HOC:

import React from "react";

export function withErrorBoundary<P>(
  WrappedComponent: React.ComponentType<P>,
  options?: {
    fallback?: React.ReactNode;
  }
) {
  return (props: P) => (
    <ErrorBoundary fallback={options?.fallback}>
      <WrappedComponent {...props} />
    </ErrorBoundary>
  );
}

/* 使用方式 */
import { withErrorBoundary } from "./withErrorBoundary";

const Dashboard = () => {
  // 可能會拋出錯誤的程式碼
  return <div>Dashboard 內容</div>;
};

export default withErrorBoundary(Dashboard, {
  fallback: <div>載入失敗,請稍後再試。</div>,
});
  • 只要把 任何 元件包在 withErrorBoundary,即自動擁有錯誤捕捉與備援 UI。
  • HOC 的 泛型 P 讓 TypeScript 正確推斷傳入的 props,避免任何型別遺失。

7. 把錯誤上傳至外部服務(以 Sentry 為例)

// src/utils/errorReporter.ts
import * as Sentry from "@sentry/react";

export function initErrorReporting() {
  Sentry.init({
    dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
    environment: process.env.NODE_ENV,
  });
}

/** 在 ErrorBoundary 中呼叫 */
export function reportError(error: Error, info: React.ErrorInfo) {
  Sentry.captureException(error, {
    extra: {
      componentStack: info.componentStack,
    },
  });
}

ErrorBoundary 中:

componentDidCatch(error: Error, info: React.ErrorInfo) {
  console.error(error);
  reportError(error, info);
}

常見陷阱與最佳實踐

陷阱 說明 解決方案
把非 UI 錯誤放在錯誤邊界 例如 fetch 的 reject,錯誤不會被 componentDidCatch 捕捉。 使用 try/catchPromise.catch,在 UI 中以 state 呈現錯誤。
render 中拋出錯誤 render 內直接 throw 會被錯誤邊界捕捉,但若是 同步副作用(如 useEffect)拋錯,則不會。 把副作用錯誤搬到 useEffect 內的 try/catch,或使用 ErrorBoundary 包住產生錯誤的子元件。
忘記為 fallback 提供 key 若 fallback 內部使用了狀態,切換錯誤後舊的狀態可能會保留下來。 <ErrorBoundary key={someId}> 上使用 唯一 key,或在 fallback 中使用 resetKey
componentDidCatch 中 setState 會觸發二次渲染,若不小心會造成 無限迴圈 僅在 getDerivedStateFromError 中更新 hasErrorcomponentDidCatch 只負責上報與副作用。
錯誤邊界本身拋錯 若錯誤邊界內部也發生錯誤,React 會 卸載整個應用 確保錯誤邊界的程式碼極簡、無外部依賴;或在 fallback 中再包一層「最小化」的錯誤邊界。

最佳實踐

  1. 最小化錯誤邊界的範圍:僅包住最可能拋錯的子樹(例如第三方 UI 元件、動態載入的模組)。
  2. 提供可自訂的 fallback:讓不同頁面可呈現符合品牌風格的錯誤畫面。
  3. 統一上報機制:所有 componentDidCatch 都呼叫同一個 reportError 函式,便於日後切換服務。
  4. 支援重置:透過 keyresetErrorBoundary 讓使用者在錯誤後能重新嘗試。
  5. 搭配 TypeScript 型別:為 errorinfofallback 定義介面,避免 any

實際應用場景

場景 為什麼需要 Error Boundary 實作要點
第三方圖表套件(如 recharts 圖表渲染時若資料格式不符會拋錯,整頁會崩潰。 把圖表元件包在 <ErrorBoundary fallback={<ChartError />}>,同時在 componentDidCatch 上報。
使用 React.lazy 動態載入 載入失敗或模組內部錯誤會導致白屏。 Suspense 外層加上 ErrorBoundary,提供「重新載入」按鈕。
表單驗證或提交 表單提交時的 API 錯誤應在 UI 中即時顯示,而非崩潰。 只在 表單元件 使用 try/catch,但如果表單內部使用了自訂 Hook 產生錯誤,則在表單外層加 ErrorBoundary
多語系文字載入 若語系檔案缺失或 JSON 解析錯誤,會拋出例外。 i18n Provider 包在錯誤邊界,fallback 顯示「語系載入失敗」。
行動裝置低記憶體環境 大量渲染時可能因記憶體不足拋錯。 在主要路由元件(如 <AppRouter>)外層加最外層的 ErrorBoundary,確保整體 App 不會因單一頁面崩潰。

總結

  • Error Boundary 是 React 用來捕捉 UI 渲染階段錯誤的唯一官方機制,配合 TypeScript 能夠寫出 型別安全、易維護 的錯誤處理層。
  • 透過 getDerivedStateFromErrorcomponentDidCatch,我們可以在 更新 state上報日誌 之間劃清界線,避免不必要的渲染迴圈。
  • 本文示範了 Class Component函式元件(react-error-boundary)HOC 三種常見寫法,並提供 Sentry 上報fallback UI重置機制 等實務技巧。
  • 常見陷阱包括 非 UI 錯誤不會被捕捉錯誤邊界本身拋錯、以及 在 componentDidCatch 中 setState 造成的迴圈,遵循最佳實踐即可有效避免。
  • 在實際專案中,將錯誤邊界 局部化自訂 fallback統一上報,並配合 Key 重置,即可大幅提升使用者體驗與系統穩定性。

把錯誤當成資訊,而不是「讓程式直接掛掉」;善用 TypeScript 的型別優勢,讓每一次例外都能被清楚追蹤、快速定位,這正是現代前端開發者不可或缺的技能。祝你在 TypeScript + React 的旅程中,寫出更安全、更友善的應用程式!