Menu

React 错误边界:用后备 UI 捕获渲染错误

错误边界是一个组件,它能捕获内部组件在渲染时抛出的错误,显示后备 UI,而不是一片空白。了解它能捕获什么、如何用类来编写、如何重置,以及 react-error-boundary 库。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

错误边界是一个 React 组件,它能捕获内部组件在渲染时抛出的错误,并显示后备 UI,而不是让整个应用崩溃。它必须是一个定义了 static getDerivedStateFromError 的类组件,因为没有对应的 Hook。你只需写一次,就能把它包在组件树的任何部分外面。

点击“Break the profile”,让子组件在渲染时抛出错误。

个人资料被后备 UI 取代,页面的其余部分照常工作。控制台会显示两次错误:React 会用 console.error 记录每个被捕获的错误,componentDidCatch 又记录了一次。“Try again”会清除错误的原因(broken)以及错误边界的错误状态,于是个人资料重新渲染。把 reset 里的 this.props.onReset(); 删掉,Try again 会立刻再次落回后备 UI,因为错误的原因没有被修复。

这个类如何工作

两个方法让一个类成为错误边界:

  • static getDerivedStateFromError(error) 在渲染期间运行,紧跟在子组件抛出错误之后。它返回让错误边界切换到后备 UI 的状态更新。它必须是纯的:不记录日志,不发请求。
  • componentDidCatch(error, info) 在后备 UI 显示到屏幕上之后运行。用它处理副作用,比如把错误发送到错误上报服务。info.componentStack 列出了抛出错误的组件以及它上层的组件。
class ErrorBoundary extends Component {
    state = { error: null };

    static getDerivedStateFromError(error) {
        return { error }; // switch to the fallback
    }

    componentDidCatch(error, info) {
        reportError(error, info.componentStack); // your logging service
    }

    render() {
        if (this.state.error) return this.props.fallback;
        return this.props.children;
    }
}

没有等价的 Hook,所以即使是只用函数组件写的应用,也需要这一个类。如果你对类的语法还不熟悉,类组件一页有详细讲解。

错误边界能捕获什么,不能捕获什么

错误边界会捕获 React 在处理其内部组件时发生的错误:

  • 渲染(函数组件的函数体、类的 render),
  • 子类组件的生命周期方法和构造函数,
  • 副作用(useEffect、useLayoutEffect)和 ref 回调,
  • 用 use 读取的被拒绝的 Promise;在 React 19 中,还包括表单 action 或传给 useTransition 的 startTransition 的函数抛出的错误。

它不会捕获:

  • 事件处理函数中的错误,
  • 你自己启动的异步代码:setTimeout、.then() 回调、事件处理函数中的 await,
  • 错误边界自身 render 中的错误(由上一层的错误边界捕获),
  • 服务器渲染期间的错误。

事件处理函数被排除在外,是因为它们不会破坏渲染。屏幕仍处于有效状态,所以在处理函数里写 try/catch 就够了。如果确实希望显示错误边界,就把错误存进状态,在下一次渲染时抛出:

第一个按钮让按钮们留在屏幕上,只在控制台打印一个未捕获的错误。第二个按钮捕获了拒绝,把它存起来,然后由错误边界接管。

错误边界放在哪里

在整个应用外面包一个错误边界能避免白屏,但之后任何小错误都会替换掉所有内容。把错误边界放在可以独立出错的部分周围:侧边栏小组件、图表、每个路由。这样一次失败只会让用户损失一个面板,而不是整个页面。

只有 Stocks 面板显示了后备 UI。把两个小组件移进同一个 <ErrorBoundary>,正常工作的 Weather 面板也会跟着消失。

如果没有任何错误边界,渲染错误未被捕获时,React 会把整棵树从屏幕上移除。空白页面比显示错误数据的损坏页面更安全,但用户会把它当成崩溃。

react-error-boundary 库

react-error-boundary 包替你封装了这个类,还加了重置辅助功能。它在这个编辑器里不可用,所以这里用静态代码块展示:

import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
    return (
        <div role="alert">
            <p>Something went wrong: {error.message}</p>
            <button onClick={resetErrorBoundary}>Try again</button>
        </div>
    );
}

<ErrorBoundary FallbackComponent={Fallback} onReset={() => refetch()} resetKeys={[userId]}>
    <Profile userId={userId} />
</ErrorBoundary>;

// Inside a child: hand an async error to the boundary
const { showBoundary } = useErrorBoundary();
fetchData().catch(showBoundary);

resetKeys 中列出的任何值发生变化时,错误边界都会重置;showBoundary 实现的就是前面示例中“存起来再抛出”的技巧。

在 React 19 中上报错误

React 19 用一种新方式报告错误。被错误边界捕获的错误会用 console.error 记录一次,未捕获的错误则交给 window.reportError。要把它们发送到你自己的服务,就给 createRoot 传入选项。每个选项都会替换掉对应的默认日志记录:

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'), {
    onCaughtError(error, errorInfo) {
        // an error boundary caught it
        logToService('caught', error, errorInfo.componentStack);
    },
    onUncaughtError(error, errorInfo) {
        // no boundary caught it, and React removed the tree
        logToService('uncaught', error, errorInfo.componentStack);
    },
    onRecoverableError(error, errorInfo) {
        // React recovered by itself, for example after a hydration mismatch
        logToService('recovered', error, errorInfo.componentStack);
    },
});
root.render(<App />);

onRecoverableError 用于 React 无需你帮忙就自行修复的错误,比如水合不匹配,或者 React 重试后就消失的渲染错误。hydrateRoot 也接受这三个选项。

常见错误

把错误边界放在会抛出错误的组件内部。 错误边界只捕获来自子组件的错误。如果 Profile 在自己的 JSX 外面渲染了 <ErrorBoundary>,却在自己的函数体里抛出错误,这个错误会越过它,交给上一层的错误边界。应该在父组件中包裹这个组件。

在 getDerivedStateFromError 中执行副作用。 它在渲染期间运行,可能不止运行一次,所以只让它返回状态。日志记录应该放在 componentDidCatch 或根节点的 onCaughtError 中。

重置了却什么都没修复。 清除错误边界的错误,会用相同的 props 渲染相同的子组件。如果它们再次抛出错误,用户又会回到后备 UI。先修复导致错误的原因(重新获取数据、清除错误输入、修改 key),再清除错误,就像第一个示例用 onReset 做的那样。

整个应用只有一个错误边界。 它能防止空白页面,却会把每个小故障都变成整页错误。在各个功能周围添加错误边界,把顶层的那个留作最后一道防线。

后备 UI 把错误藏起来,连你也看不到。 给用户显示友好的提示是对的,但要确保错误仍然进入你的日志。React 19 默认把捕获的错误记录到控制台;在生产环境中,那是用户的控制台,所以要在 componentDidCatch 或 onCaughtError 中把错误发送到服务。

错误边界天然适合和 <Suspense> 搭配:Suspense 处理“还没准备好”,错误边界处理“失败了”。像 use 示例中那样,把错误边界放在 Suspense 外面,一个组件就能涵盖加载、成功和失败三种情况。

常见问题

React 中的错误边界是什么?

一个组件,能捕获其内部任意组件在渲染时抛出的 JavaScript 错误,并渲染后备 UI。没有错误边界时,渲染期间的一个错误会让整个应用从屏幕上消失。

有用于错误边界的 Hook 吗?

没有。错误边界必须是一个带有 static getDerivedStateFromError(以及可选的 componentDidCatch)的类组件。你只需写一次,之后就能像其他组件一样在函数组件中使用它;也可以安装 react-error-boundary,它替你封装好了这个类。

错误边界能捕获事件处理函数中的错误吗?

不能。事件处理函数在渲染之外运行,所以 React 不需要错误边界来恢复。在处理函数里用 try/catch;如果希望显示错误边界,就把错误存进状态,在渲染时抛出。

错误边界能捕获异步错误吗?

你自己启动的 setTimeout、Promise 回调或 fetch().then() 中的错误捕获不到。但用 use 读取的被拒绝的 Promise 能捕获到;在 React 19 中,表单 action 或传给 useTransition 的 startTransition 的函数里抛出的错误也能捕获到。

如何重置错误边界?

先修复导致错误的原因,再清除它的错误状态,通常是通过后备 UI 中的 Try again 按钮。修改错误边界的 key 也能重置它,因为 React 会挂载一个新实例。

Coddy 编程语言插图

和 Coddy 一起学编程

开始