本文 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/catch 或 Promise.catch,在 UI 中以 state 呈現錯誤。 |
在 render 中拋出錯誤 |
render 內直接 throw 會被錯誤邊界捕捉,但若是 同步副作用(如 useEffect)拋錯,則不會。 |
把副作用錯誤搬到 useEffect 內的 try/catch,或使用 ErrorBoundary 包住產生錯誤的子元件。 |
忘記為 fallback 提供 key |
若 fallback 內部使用了狀態,切換錯誤後舊的狀態可能會保留下來。 | 在 <ErrorBoundary key={someId}> 上使用 唯一 key,或在 fallback 中使用 resetKey。 |
在 componentDidCatch 中 setState |
會觸發二次渲染,若不小心會造成 無限迴圈。 | 僅在 getDerivedStateFromError 中更新 hasError,componentDidCatch 只負責上報與副作用。 |
| 錯誤邊界本身拋錯 | 若錯誤邊界內部也發生錯誤,React 會 卸載整個應用。 | 確保錯誤邊界的程式碼極簡、無外部依賴;或在 fallback 中再包一層「最小化」的錯誤邊界。 |
最佳實踐
- 最小化錯誤邊界的範圍:僅包住最可能拋錯的子樹(例如第三方 UI 元件、動態載入的模組)。
- 提供可自訂的 fallback:讓不同頁面可呈現符合品牌風格的錯誤畫面。
- 統一上報機制:所有
componentDidCatch都呼叫同一個reportError函式,便於日後切換服務。 - 支援重置:透過
key或resetErrorBoundary讓使用者在錯誤後能重新嘗試。 - 搭配 TypeScript 型別:為
error、info、fallback定義介面,避免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 能夠寫出 型別安全、易維護 的錯誤處理層。
- 透過
getDerivedStateFromError與componentDidCatch,我們可以在 更新 state 與 上報日誌 之間劃清界線,避免不必要的渲染迴圈。 - 本文示範了 Class Component、函式元件(react-error-boundary)、HOC 三種常見寫法,並提供 Sentry 上報、fallback UI、重置機制 等實務技巧。
- 常見陷阱包括 非 UI 錯誤不會被捕捉、錯誤邊界本身拋錯、以及 在 componentDidCatch 中 setState 造成的迴圈,遵循最佳實踐即可有效避免。
- 在實際專案中,將錯誤邊界 局部化、自訂 fallback、統一上報,並配合 Key 重置,即可大幅提升使用者體驗與系統穩定性。
把錯誤當成資訊,而不是「讓程式直接掛掉」;善用 TypeScript 的型別優勢,讓每一次例外都能被清楚追蹤、快速定位,這正是現代前端開發者不可或缺的技能。祝你在 TypeScript + React 的旅程中,寫出更安全、更友善的應用程式!