错误边界与错误处理体系
第21章:错误边界与错误处理体系
React 应用中未捕获的错误会让整棵组件树崩溃——这不是 bug,是逼迫开发者主动处理错误的设计哲学。
本章核心问题:错误边界只能捕获什么类型的错误?完整的错误处理分层防御如何构建? 读完本章你将理解:
- 错误边界只捕获渲染阶段的同步错误,异步错误需要 try/catch 和 showBoundary
- 放置策略:独立功能模块互相隔离,非关键路径隔离,同一用户流程共享边界
- React 19 新增 onCaughtError/onUncaughtError/onRecoverableError 根选项
Level 1 · 你需要知道的(1-3年经验)
react-error-boundary:生产级封装
自己实现错误边界需要处理很多细节(重置状态、重试逻辑、key 重置技巧)。react-error-boundary 库提供了完善的实现:
npm install react-error-boundary
基本用法
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<p>出错了:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// 上报到 Sentry 等监控服务
reportError(error, info);
}}
onReset={() => {
// 重置应用状态(如清空缓存、重置 store)
}}
>
<UserDashboard />
</ErrorBoundary>
);
}
useErrorBoundary Hook
react-error-boundary 提供了 useErrorBoundary Hook,让函数组件能够主动触发最近父级错误边界:
import { useErrorBoundary } from 'react-error-boundary';
function DataFetcher({ userId }: { userId: string }) {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch((error) => {
// 将错误抛给最近的 ErrorBoundary
showBoundary(error);
});
}, [userId]);
// ...
}
这解决了一个重要问题:错误边界只能捕获渲染阶段的错误,而 useEffect 中的异步错误默认不会被捕获。showBoundary 让你手动将异步错误路由到错误边界。
重置错误边界
当错误是临时性的(如网络抖动),用户点击重试应该清除错误状态并重新渲染。resetKeys 是一种优雅的重置方式:
function App() {
const [location, setLocation] = useState(window.location.pathname);
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[location]} // location 变化时自动重置错误边界
onReset={() => setLocation(window.location.pathname)}
>
<Router />
</ErrorBoundary>
);
}
Level 2 · 它是怎么运行的(3-5年经验)
Error Boundary 类组件实现
错误边界必须是类组件,因为它依赖两个类组件生命周期方法,函数组件目前还没有对应的 Hook(截至 React 19,这仍是事实)。
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode | ((error: Error) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
// 静态方法:从错误中派生新状态。渲染阶段调用,必须是纯函数
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
// 实例方法:错误已确认后的副作用(日志、上报)。提交阶段调用
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError && this.state.error) {
const { fallback } = this.props;
if (typeof fallback === 'function') {
return fallback(this.state.error);
}
return fallback ?? <div>出现了一些错误</div>;
}
return this.props.children;
}
}
getDerivedStateFromError vs componentDidCatch 的分工:
getDerivedStateFromError:在渲染阶段被调用,用于更新状态使组件渲染 fallback UI。必须是纯函数,不能有副作用。componentDidCatch:在"提交阶段"(DOM 已更新后)被调用,适合做日志上报等副作用操作。
Level 3 · 规范怎么定义的(资深)
本章涉及的 API 规范和设计决策已融入各层级的讨论中。
Level 4 · 边界与陷阱(所有人)
为什么未处理的错误会让整棵树崩溃
React 在渲染时,每个组件的 render 函数(或函数组件体)都是同步执行的。如果其中一个抛出异常,React 不知道如何继续渲染该组件及其子节点——它们处于不确定状态。在 React 16 之前,React 会保留崩溃时的 UI(通常是损坏状态),这比白屏更危险,因为用户可能在损坏状态下继续操作,产生错误数据。
React 16 引入错误边界,并明确了规则:渲染阶段的未捕获错误会卸载整棵 React 树。这逼迫开发者主动处理错误,而不是寄希望于损坏的 UI 继续工作。
错误边界的放置策略
错误边界不是越多越好,也不是一个全局的就够了。正确的放置策略需要权衡:
粒度:
App
├── GlobalErrorBoundary ← 最外层:捕获所有漏网之鱼,防白屏
│ ├── Header ← Header 不需要边界(导航崩溃整个 App 都没用了)
│ ├── Sidebar
│ │ └── SidebarErrorBoundary ← 独立功能区:侧边栏崩溃不影响主内容
│ └── Main
│ ├── RouteErrorBoundary ← 路由级别:每个页面独立隔离
│ │ └── PageContent
│ │ ├── CardErrorBoundary ← 卡片级别:Dashboard 的数据卡片
│ │ └── ChartErrorBoundary ← 图表独立边界
原则:
- 独立功能模块之间要互相隔离(Dashboard 上的图表崩溃不应影响表格)
- 非关键路径要隔离(广告、推荐模块崩溃不应影响主流程)
- 同一个用户流程内部通常共享一个边界(表单填写流程崩溃就整体回退)
错误上报:集成 Sentry
npm install @sentry/react
// src/main.tsx
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 0.1, // 采样 10% 的性能追踪
replaysOnErrorSampleRate: 1, // 错误时 100% 记录回放
});
// 使用 Sentry 包装的 ErrorBoundary(自动上报)
import { ErrorBoundary } from '@sentry/react';
function App() {
return (
<ErrorBoundary
fallback={<ErrorPage />}
showDialog // 在 Sentry 上显示用户反馈对话框
>
<Router />
</ErrorBoundary>
);
}
在自定义 componentDidCatch 中手动上报:
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
Sentry.withScope((scope) => {
scope.setExtra('componentStack', errorInfo.componentStack);
scope.setTag('component', 'ErrorBoundary');
Sentry.captureException(error);
});
}
React 19 的错误处理改进
React 19 在错误处理机制上做了显著改进。
新的根选项:onCaughtError 与 onUncaughtError
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!, {
// 被错误边界捕获的错误
onCaughtError(error, errorInfo) {
console.log('被错误边界捕获:', error.message);
// errorInfo.componentStack 包含组件堆栈
},
// 未被任何错误边界捕获的错误(将导致整棵树卸载)
onUncaughtError(error, errorInfo) {
console.error('未捕获的错误,应用将崩溃:', error.message);
Sentry.captureException(error);
},
// 可恢复的错误(如 hydration 不匹配,React 会重新渲染修复)
onRecoverableError(error, errorInfo) {
console.warn('可恢复错误:', error.message);
},
});
这些回调相比之前的 window.onerror 或 window.addEventListener('error') 更精准,因为它们能区分错误的来源和严重程度。
更清晰的 Hydration 错误信息
React 19 大幅改进了 SSR hydration 错误的报错信息。之前的错误只说"hydration mismatch",React 19 会精确指出服务端和客户端渲染的内容差异:
Hydration failed because the server rendered HTML didn't match the client.
Server: <div class="theme-dark">
Client: <div class="theme-light">
这让调试 SSR 应用的类型错误变得容易得多。
异步错误处理
错误边界只能捕获渲染阶段(组件函数体、生命周期)的同步错误。以下场景需要单独处理:
事件处理器中的错误
function DeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
const [error, setError] = useState<Error | null>(null);
const { showBoundary } = useErrorBoundary();
async function handleDelete() {
try {
await onDelete();
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
if (error.name === 'NetworkError') {
// 可恢复错误:在组件内部显示提示
setError(error);
} else {
// 不可恢复错误:上抛到错误边界
showBoundary(error);
}
}
}
return (
<>
{error && <p role="alert" className="error">{error.message}</p>}
<button onClick={handleDelete}>删除</button>
</>
);
}
数据获取中的错误处理
使用 React Query 时,错误处理有完善的机制:
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
retry: 3, // 失败后自动重试 3 次
retryDelay: 1000, // 每次重试间隔 1 秒
});
if (isLoading) return <Skeleton />;
if (error) {
return (
<div role="alert">
<p>加载用户信息失败:{error.message}</p>
<button onClick={() => refetch()}>重试</button>
</div>
);
}
return <UserCard user={data} />;
}
一个完整的错误处理体系不是某个单一技术的问题,而是分层防御:渲染错误靠错误边界,异步错误靠 try/catch 和状态管理,全局兜底靠监控上报。每一层都有明确的职责,组合在一起才能让应用在面对各种失败模式时优雅降级,而不是让用户盯着白屏无从下手。