跳至主要内容
How to handle errors in React: full guide

from NADIA MAKAREVICH

Beginner

/

React / Next
JavaScript / TypeScript

本文介紹了 React 中處理錯誤的兩種方式:使用 try/catchErrorBoundary,以及它們各自的限制。使用 try/catch 時,它無法捕獲 hooks 和子組件中的錯誤。使用 ErrorBoundary 時,它無法捕獲異步代碼 (async code)事件處理程序 (event handler) 中的錯誤。但是,可以通過在 try/catch 中捕獲錯誤,然後將錯誤重新抛出到 React 的生命週期中,使 ErrorBoundary 能夠捕獲這些錯誤。

Why do we need to catch errors in React?

為什麼我們需要在 React 中捕獲錯誤呢?答案是因為從 React 16 開始,如果沒有捕獲錯誤,React 會直接在生命週期中抛出錯誤,停止整個應用程序,並跳出錯誤頁面。因此,即使錯誤發生在 UI 中的某個不重要的部分,或者是某個你無法控制的第三方庫中,如果沒有捕獲錯誤,整個頁面都會被銷毀,並且向使用者顯示錯誤頁面。

Review: Catching Errors in JavaScript

在 JavaScript 中,可以使用 try/catch 來捕獲錯誤。如果在 try 中的代碼中發生錯誤,則可以捕獲錯誤,並在 catch 中處理錯誤。這個方法適用於同步程式碼,也適用於非同步程式碼。我們也可以使用 Promise.then()Promise.catch() 來捕獲非同步的錯誤,但這篇文章以 try/catch 為主。

Catch synchronous errors
try {
doSomethingWrong();
} catch (error) {
// handle error
}
Catch asynchronous errors
try {
await fetch("/bla-bla");
} catch (error) {
// handle error
}

Problems of try/catch in React

在 React 中,我們可以使用 try/catch 來捕獲錯誤。但是,以下是三種錯誤的使用方式:

1. Catch errors in useEffect

useEffect 整個放在 try/catch 中是錯誤的。由於 useEffect 是在 render 後非同步執行,所以對於 try/catch 來說永遠都是成功執行,也就永遠抓不到 useEffect 中的錯誤。

Catch errors in hooks ❌
const Component = () => {
try {
useEffect(() => {
doSomethingWrong();
}, []);
} catch (error) {
// never triggered
}
return <div />;
};

2. Catch errors in children

children 整個放在 try/catch 中是錯誤的。不管是在 try/catch 中定義 children,還是直接在 try/catch 中返回 children,都是錯誤的。這是因為當程式執行到 <Child /> 時,它並不是真正的渲染元件,而是創建了一個元件元素。這個元素只是一個包含元件類型和屬性的對象,而且會在 try/catch 執行成功後,在 React 中被使用,與 useEffect 的情況類似,所以也是無法捕獲錯誤的。

Catch errors in children ❌
const Component = () => {
let child;

try {
child = <Child />;
} catch (e) {
// never triggered
}
return child;
};
Catch errors in children ❌
const Component = () => {
try {
return <Child />;
} catch (e) {
// never triggered
}
};

3. Catch errors in render and Change state

render 中使用 try/catch 來捕獲錯誤,並在 catch 中改變 state,這是錯誤的。這是因為 render 是同步執行的,所以 try/catch 會捕獲到錯誤,但是在 catch 中改變 state 會導致無限循環,因為 render 會再次被調用,進而再次觸發 try/catch

Change state in render ❌
const Component = () => {
const [hasError, setHasError] = useState(false);

try {
doSomethingComplicated();
} catch (e) {
// will cause infinite loop
setHasError(true);
}
};

ErrorBoundary in React

為了解決上述問題,React 提供了 ErrorBoundary 這個「概念」 來捕獲錯誤。我們必須使用舊版的 class component 自行實現一個 ErrorBoundary,然後使用它來包住可能會發生錯誤的元件。這個 ErrorBoundary 元件包含了幾個生命週期方法,例如

  • constructor 會初始化一個 state,用來存放是否發生錯誤
  • static getDerivedStateFromError 會在 render 中發生錯誤時被調用,並將 state 中的 hasError 設置為 true,我們就可以根據 hasError 來渲染錯誤訊息。這個方法是產生 ErrorBoundary 的必要方法
  • componentDidCatch 可以將錯誤訊息加以利用,例如發送到後端,或者使用 Sentry 來捕獲錯誤

另外,我們也可以自定義 ErrorBoundaryfallback,這個 fallback 會在 render 中發生錯誤時被渲染。

即時編輯器
結果
Loading...

ErrorBoundary with Asynchronous Error

上面我們所寫的 ErrorBoundary 有一些限制,例如我們不能透過 ErrorBoundary 來捕獲非同步的錯誤,例如 setTimeoutPromise;我們也無法透過 ErrorBoundary 來捕獲 Event Handler, Callback 中的錯誤,例如 onClickonSubmit

can't catch errors from event handler ❌
const onClick = () => {
// this error will disappear into the void
throw new Error("Hulk smash!");
};
can't catch errors from async function ❌
useEffect(() => {
//the error will also disappear
fetch("/bla");
}, []);

不過在 Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react. 中,Dan Abramov 提供了一個解決方案。我們可以先在 try/catch 中捕獲錯誤,然後在 catch 中觸發一次 React 重新渲染,最後再將錯誤丟回重新渲染的生命週期中。這樣 ErrorBoundary 就可以像其他錯誤一樣捕獲它們。由於 useState 的更新是觸發重新渲染的方式,而 useState 的設置函數可以接受一個更新函數作為參數,所以我們可以使用這個更新函數來重新丟出錯誤。

即時編輯器
結果
Loading...

Conclusion

在 React 中,我們無法透過 try/catch 包住整個 useEffect 或是子元件 children 來捕獲錯誤,但我們可以使用 ErrorBoundary 來達成。不過 ErrorBoundary 也有一些限制,例如它無法捕獲非同步的錯誤,例如 setTimeoutPromise;它也無法捕獲 Event Handler, Callback 中的錯誤,例如 onClickonSubmit。不過我們可以使用一個自定義的 useThrowAsyncError 來解決這個問題。我們可以在 catch 中使用 useThrowAsyncError 強制觸發 React 重新渲染,最後再將錯誤丟回重新渲染的生命週期中。

另外,我們也可以考慮使用 bvaughn/react-error-boundary 這個第三方套件來解決這個問題。這個套件提供了 <ErrorBoundary> 以及 useErrorHandler,讓我們更方便的使用 ErrorBoundary 的方式捕捉錯誤。