错误处理与边界
更新: 10/16/2025 字数: 0 字 时长: 0 分钟
什么是 Error Boundaries
Error Boundaries(错误边界)是 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) {
// 记录错误信息
console.error('捕获到错误:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>出错了!</h1>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>Error Boundaries 的工作原理
生命周期方法
1. getDerivedStateFromError
jsx
static getDerivedStateFromError(error) {
// 在渲染阶段调用
// 用于更新 state,显示备用 UI
// 不应该有副作用
return { hasError: true, error };
}2. componentDidCatch
jsx
componentDidCatch(error, errorInfo) {
// 在提交阶段调用
// 可以执行副作用,如记录错误
// error: 被抛出的错误
// errorInfo: 包含 componentStack 的对象
logErrorToService(error, errorInfo);
}两个阶段的区别
jsx
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
// Render 阶段:更新状态
static getDerivedStateFromError(error) {
console.log('getDerivedStateFromError - Render 阶段');
return { hasError: true, error };
}
// Commit 阶段:副作用
componentDidCatch(error, errorInfo) {
console.log('componentDidCatch - Commit 阶段');
// 可以在这里:
// - 记录错误日志
// - 发送错误报告
// - 更新错误统计
this.reportError(error, errorInfo);
}
reportError = (error, errorInfo) => {
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({
error: error.toString(),
componentStack: errorInfo.componentStack
})
});
};
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}Error Boundaries 能捕获什么
✅ 可以捕获
jsx
class MyComponent extends React.Component {
render() {
// 1. 渲染阶段的错误
if (this.props.data === null) {
throw new Error('数据为空');
}
return <div>{this.props.data.value}</div>;
}
componentDidMount() {
// 2. 生命周期方法中的错误
throw new Error('挂载失败');
}
handleClick = () => {
// 3. 构造函数中的错误(通过 setState 触发)
this.setState(() => {
throw new Error('状态更新失败');
});
};
}❌ 不能捕获
jsx
class MyComponent extends React.Component {
componentDidMount() {
// 1. 事件处理器中的错误
// 需要使用 try-catch
document.getElementById('btn').addEventListener('click', () => {
throw new Error('点击错误'); // 不会被捕获
});
// 2. 异步错误
setTimeout(() => {
throw new Error('异步错误'); // 不会被捕获
}, 1000);
// 3. Promise 错误
fetch('/api/data')
.then(res => {
throw new Error('Promise 错误'); // 不会被捕获
});
}
handleClick = () => {
// 4. 事件处理器中的同步错误
throw new Error('事件错误'); // 不会被捕获
};
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
// 5. Error Boundary 自身的错误
// 6. 服务端渲染的错误实现完整的 Error Boundary
基础实现
jsx
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
// 记录到错误监控服务
this.logError(error, errorInfo);
}
logError = (error, errorInfo) => {
// 发送到 Sentry、LogRocket 等
if (window.Sentry) {
window.Sentry.captureException(error, {
extra: errorInfo
});
}
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>出错了</h2>
<details>
<summary>查看详情</summary>
<p>{this.state.error && this.state.error.toString()}</p>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
</div>
);
}
return this.props.children;
}
}增强版实现
jsx
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
eventHandlerError: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
const { onError } = this.props;
this.setState({
error,
errorInfo
});
// 调用父组件的错误处理函数
if (onError) {
onError(error, errorInfo);
}
// 记录错误
this.logError(error, errorInfo);
}
logError = (error, errorInfo) => {
const errorData = {
message: error.toString(),
stack: error.stack,
componentStack: errorInfo?.componentStack,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};
// 发送到错误监控服务
fetch('/api/log-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
};
resetError = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
eventHandlerError: null
});
// 调用父组件的重置函数
if (this.props.onReset) {
this.props.onReset();
}
};
render() {
const { hasError, error, errorInfo } = this.state;
const { fallback, fallbackComponent: FallbackComponent } = this.props;
if (hasError) {
// 自定义 fallback 组件
if (FallbackComponent) {
return (
<FallbackComponent
error={error}
errorInfo={errorInfo}
resetError={this.resetError}
/>
);
}
// 自定义 fallback 渲染函数
if (fallback) {
return fallback({ error, errorInfo, resetError: this.resetError });
}
// 默认 fallback UI
return (
<div className="error-boundary">
<h2>出错了</h2>
<button onClick={this.resetError}>重试</button>
{process.env.NODE_ENV === 'development' && (
<details>
<summary>错误详情</summary>
<p>{error && error.toString()}</p>
<pre>{errorInfo?.componentStack}</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}
// 使用
<ErrorBoundary
fallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
console.log('错误:', error);
}}
onReset={() => {
// 重置应用状态
}}
>
<App />
</ErrorBoundary>使用 react-error-boundary 库
安装和基本使用
bash
npm install react-error-boundaryjsx
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>出错了:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// 记录错误
logError(error, errorInfo);
}}
onReset={() => {
// 重置状态
}}
>
<MyWidget />
</ErrorBoundary>
);
}使用 Hook
jsx
import { useErrorHandler } from 'react-error-boundary';
function MyComponent() {
const handleError = useErrorHandler();
const handleClick = async () => {
try {
await fetchData();
} catch (error) {
// 将错误传递给最近的 Error Boundary
handleError(error);
}
};
return <button onClick={handleClick}>获取数据</button>;
}错误处理策略
1. 分层错误边界
jsx
function App() {
return (
// 应用级错误边界
<ErrorBoundary
fallback={<FullPageError />}
onError={logError}
>
<Layout>
{/* 路由级错误边界 */}
<ErrorBoundary fallback={<PageError />}>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
// 功能级错误边界
<ErrorBoundary fallback={<WidgetError />}>
<Dashboard />
</ErrorBoundary>
}
/>
</Routes>
</ErrorBoundary>
</Layout>
</ErrorBoundary>
);
}2. 关键路径保护
jsx
function Dashboard() {
return (
<div>
{/* 关键组件 - 错误时显示占位符 */}
<ErrorBoundary fallback={<div>无法加载图表</div>}>
<Chart />
</ErrorBoundary>
{/* 次要组件 - 错误时隐藏 */}
<ErrorBoundary fallback={null}>
<Recommendations />
</ErrorBoundary>
{/* 关键组件 - 错误时整个页面不可用 */}
<DataTable />
</div>
);
}3. 重试机制
jsx
class RetryErrorBoundary extends React.Component {
state = {
hasError: false,
retryCount: 0
};
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
handleRetry = () => {
this.setState(state => ({
hasError: false,
retryCount: state.retryCount + 1
}));
};
render() {
const { hasError, retryCount } = this.state;
const { maxRetries = 3 } = this.props;
if (hasError) {
if (retryCount >= maxRetries) {
return <div>重试次数过多,请刷新页面</div>;
}
return (
<div>
<p>加载失败</p>
<button onClick={this.handleRetry}>
重试 ({retryCount + 1}/{maxRetries})
</button>
</div>
);
}
return this.props.children;
}
}4. 自动恢复
jsx
class AutoRecoveryErrorBoundary extends React.Component {
state = { hasError: false };
timeoutId = null;
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
// 5 秒后自动重试
this.timeoutId = setTimeout(() => {
this.setState({ hasError: false });
}, 5000);
}
componentWillUnmount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
render() {
if (this.state.hasError) {
return <div>出错了,5 秒后自动重试...</div>;
}
return this.props.children;
}
}处理异步错误
1. 事件处理器错误
jsx
function MyComponent() {
const handleError = useErrorHandler();
const handleClick = () => {
try {
// 可能出错的代码
riskyOperation();
} catch (error) {
handleError(error);
}
};
return <button onClick={handleClick}>点击</button>;
}2. useEffect 中的错误
jsx
function MyComponent() {
const handleError = useErrorHandler();
useEffect(() => {
fetchData()
.catch(error => {
handleError(error);
});
}, [handleError]);
return <div>Loading...</div>;
}3. 自定义 Hook 处理异步错误
jsx
function useAsyncError() {
const [, setError] = useState();
return useCallback(
error => {
setError(() => {
throw error;
});
},
[]
);
}
// 使用
function MyComponent() {
const throwError = useAsyncError();
useEffect(() => {
fetchData().catch(throwError);
}, [throwError]);
return <div>Loading...</div>;
}错误监控和上报
集成 Sentry
jsx
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'your-sentry-dsn',
integrations: [new Sentry.BrowserTracing()],
tracesSampleRate: 1.0,
});
// 使用 Sentry 的 ErrorBoundary
function App() {
return (
<Sentry.ErrorBoundary
fallback={ErrorFallback}
showDialog
>
<MyWidget />
</Sentry.ErrorBoundary>
);
}自定义错误上报
jsx
class ErrorReporter {
static report(error, errorInfo, context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
context,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
userId: getCurrentUserId(),
sessionId: getSessionId()
};
// 发送到服务器
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
// 本地存储(离线时)
if (!navigator.onLine) {
const errors = JSON.parse(
localStorage.getItem('offlineErrors') || '[]'
);
errors.push(errorData);
localStorage.setItem('offlineErrors', JSON.stringify(errors));
}
}
static reportSyncErrors() {
const errors = JSON.parse(
localStorage.getItem('offlineErrors') || '[]'
);
if (errors.length > 0) {
fetch('/api/errors/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errors)
}).then(() => {
localStorage.removeItem('offlineErrors');
});
}
}
}
// 监听在线状态
window.addEventListener('online', () => {
ErrorReporter.reportSyncErrors();
});开发环境 vs 生产环境
区分环境的错误显示
jsx
class ErrorBoundary extends React.Component {
render() {
if (this.state.hasError) {
// 开发环境:显示详细错误
if (process.env.NODE_ENV === 'development') {
return (
<div>
<h2>开发环境错误</h2>
<details open>
<summary>错误详情</summary>
<pre>{this.state.error?.toString()}</pre>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
</div>
);
}
// 生产环境:显示友好提示
return (
<div>
<h2>出错了</h2>
<p>我们已经收到错误报告,将尽快修复。</p>
<button onClick={this.resetError}>返回首页</button>
</div>
);
}
return this.props.children;
}
}最佳实践
1. 错误边界粒度
jsx
// ✅ 好:合适的粒度
function App() {
return (
<ErrorBoundary>
<Layout>
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<Main />
</ErrorBoundary>
</Layout>
</ErrorBoundary>
);
}
// ❌ 不好:粒度太细
function App() {
return (
<ErrorBoundary>
<ErrorBoundary>
<ErrorBoundary>
<button>Click</button>
</ErrorBoundary>
</ErrorBoundary>
</ErrorBoundary>
);
}2. 提供有意义的错误信息
jsx
// ❌ 不好
throw new Error('错误');
// ✅ 好
throw new Error(`无法加载用户数据:用户 ID ${userId} 不存在`);3. 记录上下文信息
jsx
componentDidCatch(error, errorInfo) {
logError({
error,
errorInfo,
context: {
userId: this.props.userId,
page: this.props.location.pathname,
action: this.state.lastAction
}
});
}4. 优雅降级
jsx
<ErrorBoundary
fallback={
<div className="widget-error">
<p>此功能暂时不可用</p>
<p>您可以继续使用其他功能</p>
</div>
}
>
<OptionalWidget />
</ErrorBoundary>总结
错误处理的核心要点:
Error Boundaries
- 只能捕获子组件的渲染错误
- 不能捕获事件处理器、异步错误
- 使用类组件实现
两个生命周期
getDerivedStateFromError:更新状态componentDidCatch:记录错误
错误处理策略
- 分层错误边界
- 关键路径保护
- 重试机制
- 自动恢复
最佳实践
- 合适的错误边界粒度
- 提供有意义的错误信息
- 记录上下文信息
- 优雅降级
良好的错误处理有助于:
- 提高应用的稳定性
- 改善用户体验
- 快速定位和修复问题
- 收集错误数据用于改进