Appearance
错误边界深入 - React错误处理完全指南
1. 错误边界基础
1.1 什么是错误边界
typescript
const errorBoundaryDefinition = {
定义: 'React组件,可以捕获其子组件树中的JavaScript错误,记录错误,并显示备用UI',
作用: [
'防止整个应用崩溃',
'提供优雅的错误处理',
'记录错误日志',
'提升用户体验'
],
限制: [
'不能捕获事件处理器中的错误',
'不能捕获异步代码中的错误(setTimeout, Promise等)',
'不能捕获服务端渲染的错误',
'不能捕获错误边界自身的错误'
],
使用场景: [
'保护关键UI区域',
'第三方组件包裹',
'路由级别错误处理',
'懒加载组件保护'
]
};1.2 基础实现
jsx
// 类组件实现错误边界
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
// 渲染备用UI
static getDerivedStateFromError(error) {
return { hasError: true };
}
// 记录错误信息
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.setState({
error,
errorInfo
});
// 发送错误到日志服务
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
{process.env.NODE_ENV === 'development' && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}1.3 函数组件错误边界(实验性)
jsx
// 使用react-error-boundary库
import { ErrorBoundary } 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, errorInfo) => {
console.error('Error:', error, errorInfo);
}}
onReset={() => {
// 重置应用状态
}}
>
<MyComponent />
</ErrorBoundary>
);
}
// 使用render prop
function AppWithRenderProp() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Reset</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>
);
}2. 高级错误边界实现
2.1 可配置的错误边界
jsx
class ConfigurableErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
const { onError, maxRetries = 3 } = this.props;
this.setState(prevState => ({
error,
errorInfo,
errorCount: prevState.errorCount + 1
}));
// 调用onError回调
if (onError) {
onError(error, errorInfo);
}
// 自动重试
if (this.state.errorCount < maxRetries && this.props.autoRetry) {
setTimeout(() => {
this.reset();
}, this.props.retryDelay || 1000);
}
}
reset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
const { hasError, error, errorInfo, errorCount } = this.state;
const {
fallback,
fallbackComponent: FallbackComponent,
children,
showDetails = process.env.NODE_ENV === 'development'
} = this.props;
if (hasError) {
// 自定义Fallback组件
if (FallbackComponent) {
return (
<FallbackComponent
error={error}
errorInfo={errorInfo}
errorCount={errorCount}
reset={this.reset}
/>
);
}
// Fallback元素
if (fallback) {
return fallback;
}
// 默认Fallback UI
return (
<div style={{ padding: '20px', border: '1px solid red' }}>
<h2>Error Occurred</h2>
<p>Attempt {errorCount}</p>
<button onClick={this.reset}>Retry</button>
{showDetails && (
<details style={{ whiteSpace: 'pre-wrap', marginTop: '10px' }}>
<summary>Error Details</summary>
<p>{error && error.toString()}</p>
<p>{errorInfo && errorInfo.componentStack}</p>
</details>
)}
</div>
);
}
return children;
}
}
// 使用
function App() {
const handleError = (error, errorInfo) => {
// 发送到错误追踪服务
Sentry.captureException(error, { extra: errorInfo });
};
return (
<ConfigurableErrorBoundary
onError={handleError}
maxRetries={3}
autoRetry={true}
retryDelay={2000}
showDetails={true}
>
<MyComponent />
</ConfigurableErrorBoundary>
);
}2.2 多级错误边界
jsx
// 顶层错误边界: 捕获整个应用的错误
class RootErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('[Root] Error:', error, errorInfo);
logErrorToService(error, { level: 'critical', ...errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<h1>Application Error</h1>
<p>We're sorry, something went wrong.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// 功能区域错误边界: 保护特定功能
class FeatureErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(`[Feature: ${this.props.featureName}] Error:`, error);
logErrorToService(error, {
level: 'warning',
feature: this.props.featureName,
...errorInfo
});
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', background: '#ffe6e6' }}>
<h3>{this.props.featureName} Error</h3>
<p>This feature is temporarily unavailable.</p>
</div>
);
}
return this.props.children;
}
}
// 组件级错误边界: 保护单个组件
class ComponentErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(`[Component: ${this.props.componentName}] Error:`, error);
}
render() {
if (this.state.hasError) {
return this.props.fallback || null;
}
return this.props.children;
}
}
// 应用结构
function App() {
return (
<RootErrorBoundary>
<Header />
<main>
<FeatureErrorBoundary featureName="User Dashboard">
<Dashboard />
</FeatureErrorBoundary>
<FeatureErrorBoundary featureName="Notifications">
<ComponentErrorBoundary
componentName="NotificationPanel"
fallback={<div>Notifications unavailable</div>}
>
<NotificationPanel />
</ComponentErrorBoundary>
</FeatureErrorBoundary>
<FeatureErrorBoundary featureName="Chat">
<ChatWidget />
</FeatureErrorBoundary>
</main>
<Footer />
</RootErrorBoundary>
);
}2.3 错误边界工厂
jsx
// 创建统一的错误边界工厂
function createErrorBoundary(config = {}) {
const {
name = 'ErrorBoundary',
onError = () => {},
fallback = null,
level = 'component' // 'root', 'feature', 'component'
} = config;
return class extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo });
onError(error, errorInfo, { name, level });
}
reset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render() {
if (this.state.hasError) {
if (typeof fallback === 'function') {
return fallback({
error: this.state.error,
errorInfo: this.state.errorInfo,
reset: this.reset
});
}
return fallback;
}
return this.props.children;
}
};
}
// 使用工厂创建不同级别的错误边界
const RootErrorBoundary = createErrorBoundary({
name: 'Root',
level: 'root',
onError: (error, errorInfo, context) => {
Sentry.captureException(error, {
level: 'fatal',
contexts: { react: errorInfo, custom: context }
});
},
fallback: ({ reset }) => (
<div>
<h1>Fatal Error</h1>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
)
});
const FeatureErrorBoundary = createErrorBoundary({
name: 'Feature',
level: 'feature',
onError: (error, errorInfo, context) => {
Sentry.captureException(error, {
level: 'error',
contexts: { react: errorInfo, custom: context }
});
},
fallback: ({ error, reset }) => (
<div>
<p>Feature Error: {error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)
});3. 错误边界最佳实践
3.1 路由级别错误边界
jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
// 路由错误边界包装器
function RouteErrorBoundary({ children }) {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<h2>Page Error</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Go Back</button>
</div>
)}
onError={(error, errorInfo) => {
console.error('Route Error:', error);
logErrorToService(error, { route: window.location.pathname, ...errorInfo });
}}
>
<Suspense fallback={<div>Loading...</div>}>
{children}
</Suspense>
</ErrorBoundary>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<RouteErrorBoundary>
<Home />
</RouteErrorBoundary>
}
/>
<Route
path="/about"
element={
<RouteErrorBoundary>
<About />
</RouteErrorBoundary>
}
/>
<Route
path="/dashboard"
element={
<RouteErrorBoundary>
<Dashboard />
</RouteErrorBoundary>
}
/>
</Routes>
</BrowserRouter>
);
}3.2 异步错误处理
jsx
// 错误边界不能捕获异步错误,需要手动处理
function AsyncComponent() {
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData()
.then(setData)
.catch(setError) // 捕获异步错误
.finally(() => setLoading(false));
}, []);
// 手动抛出错误,让错误边界捕获
if (error) {
throw error;
}
if (loading) return <div>Loading...</div>;
return <div>{data}</div>;
}
// 更优雅的方式: 自定义Hook
function useAsyncError() {
const [, setError] = useState();
return useCallback((error) => {
setError(() => {
throw error;
});
}, []);
}
function AsyncComponentWithHook() {
const throwError = useAsyncError();
const [data, setData] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(throwError); // 抛给错误边界
}, [throwError]);
return <div>{data}</div>;
}
// React Query自动处理异步错误
import { useQuery } from '@tanstack/react-query';
function AsyncComponentWithReactQuery() {
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
useErrorBoundary: true // 抛给错误边界
});
return <div>{data}</div>;
}3.3 事件处理器错误
jsx
// 事件处理器中的错误不会被错误边界捕获
function BadEventHandler() {
const handleClick = () => {
throw new Error('Event handler error'); // 不会被捕获!
};
return <button onClick={handleClick}>Click Me</button>;
}
// 解决方案1: try-catch
function GoodEventHandlerSolution1() {
const handleClick = () => {
try {
throw new Error('Event handler error');
} catch (error) {
console.error('Error in event handler:', error);
// 显示错误UI或通知用户
}
};
return <button onClick={handleClick}>Click Me</button>;
}
// 解决方案2: 错误状态
function GoodEventHandlerSolution2() {
const [error, setError] = useState(null);
const handleClick = () => {
try {
throw new Error('Event handler error');
} catch (err) {
setError(err);
}
};
if (error) {
throw error; // 让错误边界捕获
}
return <button onClick={handleClick}>Click Me</button>;
}
// 解决方案3: 高阶函数包装
function withErrorHandling(handler) {
return async (...args) => {
try {
await handler(...args);
} catch (error) {
console.error('Event handler error:', error);
// 显示错误通知
showErrorToast(error.message);
}
};
}
function Component() {
const handleClick = withErrorHandling(async () => {
await riskyOperation();
});
return <button onClick={handleClick}>Click Me</button>;
}4. 错误日志和监控
4.1 集成错误追踪服务
jsx
// Sentry集成
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: process.env.NODE_ENV,
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay()
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0
});
// 使用Sentry的ErrorBoundary
function App() {
return (
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
)}
showDialog
>
<MyApp />
</Sentry.ErrorBoundary>
);
}
// 手动捕获错误
function Component() {
const handleClick = () => {
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { section: 'user-action' },
extra: { userId: getCurrentUserId() }
});
}
};
return <button onClick={handleClick}>Do Something</button>;
}4.2 自定义错误日志系统
jsx
// 错误日志管理器
class ErrorLogger {
constructor() {
this.errors = [];
this.maxErrors = 100;
}
log(error, errorInfo, context = {}) {
const errorLog = {
id: Date.now(),
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
name: error.name
},
errorInfo,
context: {
url: window.location.href,
userAgent: navigator.userAgent,
...context
}
};
this.errors.push(errorLog);
// 限制错误数量
if (this.errors.length > this.maxErrors) {
this.errors.shift();
}
// 发送到服务器
this.sendToServer(errorLog);
// 本地存储
this.saveToLocalStorage();
}
sendToServer(errorLog) {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorLog)
}).catch(err => {
console.error('Failed to send error log:', err);
});
}
saveToLocalStorage() {
try {
localStorage.setItem('errorLogs', JSON.stringify(this.errors));
} catch (err) {
console.error('Failed to save error logs:', err);
}
}
getErrors() {
return this.errors;
}
clearErrors() {
this.errors = [];
localStorage.removeItem('errorLogs');
}
}
const errorLogger = new ErrorLogger();
// 在错误边界中使用
class ErrorBoundaryWithLogging extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
errorLogger.log(error, errorInfo, {
component: this.props.componentName,
userId: this.props.userId
});
}
render() {
if (this.state.hasError) {
return <div>Error occurred</div>;
}
return this.props.children;
}
}4.3 错误分析和可视化
jsx
// 错误统计Dashboard
function ErrorDashboard() {
const [errors, setErrors] = useState([]);
const [stats, setStats] = useState({});
useEffect(() => {
// 获取错误日志
const logs = errorLogger.getErrors();
setErrors(logs);
// 计算统计信息
const statistics = {
total: logs.length,
byType: {},
byHour: {},
topErrors: {}
};
logs.forEach(log => {
// 按类型统计
const type = log.error.name;
statistics.byType[type] = (statistics.byType[type] || 0) + 1;
// 按小时统计
const hour = new Date(log.timestamp).getHours();
statistics.byHour[hour] = (statistics.byHour[hour] || 0) + 1;
// Top错误
const message = log.error.message;
statistics.topErrors[message] = (statistics.topErrors[message] || 0) + 1;
});
setStats(statistics);
}, []);
return (
<div>
<h2>Error Dashboard</h2>
<section>
<h3>总体统计</h3>
<p>总错误数: {stats.total}</p>
</section>
<section>
<h3>按类型</h3>
<ul>
{Object.entries(stats.byType || {}).map(([type, count]) => (
<li key={type}>{type}: {count}</li>
))}
</ul>
</section>
<section>
<h3>Top错误</h3>
<ul>
{Object.entries(stats.topErrors || {})
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([message, count]) => (
<li key={message}>{message}: {count}次</li>
))}
</ul>
</section>
<section>
<h3>最近错误</h3>
<table>
<thead>
<tr>
<th>时间</th>
<th>类型</th>
<th>消息</th>
</tr>
</thead>
<tbody>
{errors.slice(-20).reverse().map(log => (
<tr key={log.id}>
<td>{new Date(log.timestamp).toLocaleString()}</td>
<td>{log.error.name}</td>
<td>{log.error.message}</td>
</tr>
))}
</tbody>
</table>
</section>
</div>
);
}5. 错误恢复策略
5.1 自动重试
jsx
class RetryErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
retryCount: 0
};
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
const { maxRetries = 3, retryDelay = 1000 } = this.props;
if (this.state.retryCount < maxRetries) {
setTimeout(() => {
this.setState(prevState => ({
hasError: false,
retryCount: prevState.retryCount + 1
}));
}, retryDelay * (this.state.retryCount + 1)); // 指数退避
}
}
render() {
const { hasError, retryCount } = this.state;
const { maxRetries = 3 } = this.props;
if (hasError && retryCount >= maxRetries) {
return (
<div>
<p>Failed after {maxRetries} attempts</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
if (hasError) {
return <div>Retrying... ({retryCount + 1}/{maxRetries})</div>;
}
return this.props.children;
}
}5.2 降级方案
jsx
// 优雅降级的组件
function RobustComponent({ primaryComponent: Primary, fallbackComponent: Fallback }) {
const [useFallback, setUseFallback] = useState(false);
if (useFallback && Fallback) {
return <Fallback />;
}
return (
<ErrorBoundary
onError={() => setUseFallback(true)}
fallback={Fallback ? <Fallback /> : <div>Feature unavailable</div>}
>
<Primary />
</ErrorBoundary>
);
}
// 使用
function App() {
return (
<RobustComponent
primaryComponent={AdvancedChart}
fallbackComponent={SimpleChart}
/>
);
}5.3 用户反馈
jsx
// 带用户反馈的错误边界
class UserFriendlyErrorBoundary extends React.Component {
state = { hasError: false, feedbackSent: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.error = error;
this.errorInfo = errorInfo;
}
sendFeedback = (userFeedback) => {
const errorReport = {
error: this.error.toString(),
errorInfo: this.errorInfo,
userFeedback,
timestamp: new Date().toISOString()
};
fetch('/api/error-feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorReport)
}).then(() => {
this.setState({ feedbackSent: true });
});
};
render() {
if (this.state.hasError) {
if (this.state.feedbackSent) {
return (
<div>
<h3>Thank you for your feedback!</h3>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return (
<ErrorFeedbackForm
error={this.error}
onSubmit={this.sendFeedback}
/>
);
}
return this.props.children;
}
}
function ErrorFeedbackForm({ error, onSubmit }) {
const [feedback, setFeedback] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(feedback);
};
return (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<form onSubmit={handleSubmit}>
<label>
What were you trying to do?
<textarea
value={feedback}
onChange={e => setFeedback(e.target.value)}
placeholder="Describe what happened..."
/>
</label>
<button type="submit">Send Feedback</button>
</form>
</div>
);
}6. 测试错误边界
jsx
// 测试错误边界
import { render, screen } from '@testing-library/react';
describe('ErrorBoundary', () => {
// 抑制控制台错误
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterAll(() => {
console.error.mockRestore();
});
it('catches errors and displays fallback UI', () => {
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary fallback={<div>Error occurred</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Error occurred')).toBeInTheDocument();
});
it('calls onError callback', () => {
const onError = jest.fn();
const ThrowError = () => {
throw new Error('Test error');
};
render(
<ErrorBoundary onError={onError}>
<ThrowError />
</ErrorBoundary>
);
expect(onError).toHaveBeenCalled();
});
it('resets error boundary', () => {
const ThrowError = ({ shouldThrow }) => {
if (shouldThrow) {
throw new Error('Test error');
}
return <div>No error</div>;
};
const { rerender } = render(
<ErrorBoundary>
<ThrowError shouldThrow={true} />
</ErrorBoundary>
);
expect(screen.getByText(/error/i)).toBeInTheDocument();
// 重置错误
rerender(
<ErrorBoundary>
<ThrowError shouldThrow={false} />
</ErrorBoundary>
);
expect(screen.getByText('No error')).toBeInTheDocument();
});
});7. 总结
错误边界是React应用错误处理的核心机制:
- 基础: 使用类组件实现,提供getDerivedStateFromError和componentDidCatch
- 高级: 多级错误边界、可配置错误边界、错误边界工厂
- 最佳实践: 路由级别、异步错误、事件处理器错误
- 监控: Sentry集成、自定义日志、错误分析
- 恢复: 自动重试、降级方案、用户反馈
- 测试: 完整的测试覆盖
合理使用错误边界可以显著提升应用的健壮性和用户体验。