错误边界与降级
优雅处理 React 应用中的错误
1. Error Boundary 基础
1.1 什么是 Error Boundary?
Error Boundary 是一种 React 组件,可以捕获子组件树中的 JavaScript 错误,记录错误,并显示降级 UI。
jsx
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state,下次渲染显示降级 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 记录错误信息
logErrorToService(error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}1.2 使用方式
jsx
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
// 嵌套使用,更细粒度的错误隔离
<ErrorBoundary fallback={<AppError />}>
<Header />
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>1.3 Error Boundary 不能捕获的错误
┌─────────────────────────────────────────────────────────────────┐
│ Error Boundary 无法捕获 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 事件处理器中的错误(需要 try-catch) │
│ 2. 异步代码(setTimeout、fetch 等) │
│ 3. 服务端渲染(SSR) │
│ 4. Error Boundary 自身的错误 │
│ │
└─────────────────────────────────────────────────────────────────┘2. 实用 Error Boundary 实现
2.1 带重试功能
jsx
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return this.props.fallback ? (
this.props.fallback({
error: this.state.error,
reset: this.handleReset
})
) : (
<div>
<h2>出错了</h2>
<button onClick={this.handleReset}>重试</button>
</div>
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary
fallback={({ error, reset }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>重试</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>2.2 使用 react-error-boundary 库
jsx
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// 上报错误
logError(error, info);
}}
onReset={() => {
// 重置应用状态
}}
>
<MyComponent />
</ErrorBoundary>
);
}
// 在子组件中主动触发错误边界
function MyComponent() {
const { showBoundary } = useErrorBoundary();
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
showBoundary(error); // 触发最近的 Error Boundary
}
};
return <button onClick={handleClick}>Do something risky</button>;
}3. 函数组件中的错误处理
3.1 事件处理器错误
jsx
function MyComponent() {
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
// 方式一:显示错误状态
setError(error);
// 方式二:使用 Toast 通知
showToast(error.message);
// 方式三:上报错误
reportError(error);
}
};
return <button onClick={handleClick}>Click</button>;
}3.2 useAsyncError Hook
jsx
// 将异步错误转换为渲染错误,让 Error Boundary 捕获
function useAsyncError() {
const [, setError] = useState();
return useCallback((error) => {
setError(() => {
throw error; // 在渲染中抛出错误
});
}, []);
}
// 使用
function MyComponent() {
const throwError = useAsyncError();
useEffect(() => {
fetchData()
.catch(throwError); // 异步错误会被 Error Boundary 捕获
}, []);
return <div>...</div>;
}4. 降级策略
4.1 分层降级
jsx
// 全局降级
function App() {
return (
<ErrorBoundary fallback={<GlobalErrorPage />}>
<Layout>
{/* 页面级降级 */}
<ErrorBoundary fallback={<PageErrorFallback />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={
{/* 组件级降级 */}
<ErrorBoundary fallback={<WidgetError />}>
<Dashboard />
</ErrorBoundary>
} />
</Routes>
</ErrorBoundary>
</Layout>
</ErrorBoundary>
);
}4.2 优雅降级 vs 渐进增强
jsx
// 优雅降级:功能不可用时显示替代内容
function FeatureWithFallback() {
const [error, setError] = useState(null);
if (error) {
return <SimplifiedVersion />; // 降级到简化版本
}
return (
<ErrorBoundary onError={setError}>
<AdvancedFeature />
</ErrorBoundary>
);
}
// 渐进增强:基础功能始终可用,增强功能可选
function ProgressiveFeature() {
const [enhanced, setEnhanced] = useState(false);
useEffect(() => {
// 检测浏览器是否支持增强功能
if (supportsAdvancedFeature()) {
setEnhanced(true);
}
}, []);
return enhanced ? <EnhancedVersion /> : <BasicVersion />;
}4.3 占位内容
jsx
// 内容不可用时显示占位
function ContentWithPlaceholder() {
return (
<ErrorBoundary
fallback={
<div className="placeholder">
<img src="/placeholder.svg" alt="" />
<p>内容暂时不可用</p>
</div>
}
>
<DynamicContent />
</ErrorBoundary>
);
}5. 错误上报
5.1 错误信息收集
jsx
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// 收集错误信息
const errorReport = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
userId: getCurrentUserId(),
};
// 上报到监控服务
reportToSentry(errorReport);
// 或其他服务
// reportToLogRocket(errorReport);
}
}5.2 集成 Sentry
jsx
import * as Sentry from '@sentry/react';
// 初始化
Sentry.init({
dsn: 'your-sentry-dsn',
environment: process.env.NODE_ENV,
});
// 使用 Sentry 的 Error Boundary
function App() {
return (
<Sentry.ErrorBoundary fallback={<ErrorFallback />}>
<MyApp />
</Sentry.ErrorBoundary>
);
}
// 手动上报
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error);
}6. 常见错误类型处理
6.1 网络错误
jsx
function DataFetcher() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
setError(err);
// 自动重试(最多 3 次)
if (retryCount < 3) {
setTimeout(() => {
setRetryCount(c => c + 1);
fetchData();
}, 1000 * Math.pow(2, retryCount)); // 指数退避
}
}
};
if (error && retryCount >= 3) {
return <NetworkErrorUI error={error} onRetry={() => {
setRetryCount(0);
fetchData();
}} />;
}
return data ? <DataDisplay data={data} /> : <Loading />;
}6.2 权限错误
jsx
function ProtectedContent() {
const { user, error } = useAuth();
if (error?.code === 'UNAUTHORIZED') {
return <Redirect to="/login" />;
}
if (error?.code === 'FORBIDDEN') {
return <AccessDenied />;
}
return <SecretContent />;
}6.3 表单验证错误
jsx
function Form() {
const [errors, setErrors] = useState({});
const handleSubmit = async (data) => {
try {
await submitForm(data);
} catch (error) {
if (error.validationErrors) {
// 显示字段级错误
setErrors(error.validationErrors);
} else {
// 显示通用错误
showToast(error.message);
}
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" />
{errors.email && <span className="error">{errors.email}</span>}
<button>Submit</button>
</form>
);
}7. Next.js 中的错误处理
7.1 error.tsx
tsx
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 上报错误
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}7.2 global-error.tsx
tsx
// app/global-error.tsx - 处理根布局错误
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}8. 面试高频问题
Q1: Error Boundary 能捕获哪些错误?
能捕获:
- 子组件渲染期间的错误
- 生命周期方法中的错误
- 构造函数中的错误
不能捕获:
- 事件处理器
- 异步代码(setTimeout, Promise)
- 服务端渲染
- Error Boundary 自身的错误
Q2: 为什么 Error Boundary 必须用 Class 组件?
因为 getDerivedStateFromError 和 componentDidCatch 是 Class 组件的生命周期方法,目前没有对应的 Hooks。不过可以用 react-error-boundary 库在函数组件中使用。
Q3: 如何在事件处理器中处理错误?
jsx
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
// 1. 设置错误状态
setError(error);
// 2. 显示 Toast
showToast(error.message);
// 3. 使用 useErrorBoundary
showBoundary(error);
}
};Q4: 错误降级的最佳实践?
- 分层错误边界:全局、页面、组件三层
- 有意义的降级 UI:不要只显示"出错了"
- 提供重试机制:让用户可以恢复
- 保持部分功能可用:隔离错误影响范围
- 记录错误信息:便于排查问题
Q5: 如何测试 Error Boundary?
jsx
import { render, screen } from '@testing-library/react';
// 抑制 console.error
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
test('renders fallback on error', () => {
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
spy.mockRestore();