Appearance
错误处理与重试
概述
在数据获取过程中,错误处理和重试机制是确保应用稳定性和用户体验的关键。从网络错误到服务器异常,合理的错误处理策略能够让应用更加健壮。本文将深入探讨React应用中的错误处理和重试策略。
错误类型
网络错误
jsx
import { useState, useEffect } from 'react';
function NetworkErrorHandling() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [errorType, setErrorType] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
setData(json);
} catch (error) {
// 网络错误判断
if (error instanceof TypeError && error.message.includes('fetch')) {
setErrorType('NETWORK_ERROR');
setError('网络连接失败,请检查您的网络设置');
}
// HTTP错误
else if (error.message.startsWith('HTTP')) {
setErrorType('HTTP_ERROR');
setError(error.message);
}
// 其他错误
else {
setErrorType('UNKNOWN_ERROR');
setError('未知错误: ' + error.message);
}
}
};
fetchData();
}, []);
if (error) {
return (
<div className={`error error-${errorType.toLowerCase()}`}>
<h3>错误</h3>
<p>{error}</p>
{errorType === 'NETWORK_ERROR' && (
<button onClick={() => window.location.reload()}>
重新加载
</button>
)}
</div>
);
}
return <div>{JSON.stringify(data)}</div>;
}HTTP状态码错误
jsx
function HttpErrorHandling() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
// 根据状态码处理
switch (response.status) {
case 200:
const json = await response.json();
setData(json);
break;
case 400:
const badRequest = await response.json();
throw new Error(`请求参数错误: ${badRequest.message}`);
case 401:
// 未授权,跳转登录
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
throw new Error('没有权限访问此资源');
case 404:
throw new Error('请求的资源不存在');
case 429:
throw new Error('请求过于频繁,请稍后再试');
case 500:
throw new Error('服务器内部错误');
case 502:
case 503:
case 504:
throw new Error('服务暂时不可用,请稍后再试');
default:
throw new Error(`未知错误: ${response.status}`);
}
} catch (error) {
setError(error.message);
}
};
fetchData();
}, []);
if (error) return <div className="error">{error}</div>;
return <div>{JSON.stringify(data)}</div>;
}超时错误
jsx
function TimeoutHandling() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const fetchWithTimeout = async (url, timeout = 5000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接');
}
throw error;
}
};
useEffect(() => {
const fetchData = async () => {
try {
const result = await fetchWithTimeout(
'https://api.example.com/data',
3000 // 3秒超时
);
setData(result);
} catch (error) {
setError(error.message);
}
};
fetchData();
}, []);
if (error) {
return (
<div className="error">
<p>{error}</p>
<button onClick={() => window.location.reload()}>
重试
</button>
</div>
);
}
return <div>{JSON.stringify(data)}</div>;
}错误边界
Error Boundary组件
jsx
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.setState({
error,
errorInfo,
});
// 发送错误到监控服务
logErrorToService(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>出错了</h2>
<details>
<summary>错误详情</summary>
<pre>{this.state.error?.toString()}</pre>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
<button onClick={this.handleReset}>重试</button>
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary>
<DataFetchingComponent />
</ErrorBoundary>
);
}
function logErrorToService(error, errorInfo) {
// 发送到Sentry、LogRocket等服务
console.log('Logging error:', error, errorInfo);
}React Query错误边界
jsx
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-fallback">
<h2>数据加载失败</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={reset}
>
<DataComponent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}重试机制
基础重试
jsx
function BasicRetry() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchWithRetry = async (url, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
// 最后一次重试失败
if (i === retries - 1) {
throw error;
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
};
const fetchData = async () => {
try {
const result = await fetchWithRetry('https://api.example.com/data');
setData(result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}指数退避重试
jsx
function ExponentialBackoffRetry() {
const [data, setData] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const [error, setError] = useState(null);
useEffect(() => {
const fetchWithExponentialBackoff = async (
url,
maxRetries = 5,
baseDelay = 1000
) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
// 指数退避: 1s, 2s, 4s, 8s, 16s
const delay = baseDelay * Math.pow(2, i);
// 添加抖动(jitter)避免同时重试
const jitter = Math.random() * 0.3 * delay;
const totalDelay = delay + jitter;
setRetryCount(i + 1);
await new Promise(resolve => setTimeout(resolve, totalDelay));
}
}
};
const fetchData = async () => {
try {
const result = await fetchWithExponentialBackoff(
'https://api.example.com/data'
);
setData(result);
setRetryCount(0);
} catch (error) {
setError(error.message);
}
};
fetchData();
}, []);
if (error) {
return (
<div className="error">
<p>Error: {error}</p>
<p>已重试 {retryCount} 次</p>
</div>
);
}
if (retryCount > 0) {
return <div>重试中... (第 {retryCount} 次)</div>;
}
return <div>{JSON.stringify(data)}</div>;
}条件重试
jsx
class RetryStrategy {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.baseDelay = options.baseDelay || 1000;
this.retryableStatuses = options.retryableStatuses || [408, 429, 500, 502, 503, 504];
this.retryableErrors = options.retryableErrors || ['ECONNRESET', 'ETIMEDOUT'];
}
shouldRetry(error, attempt) {
if (attempt >= this.maxRetries) {
return false;
}
// 网络错误总是重试
if (error.name === 'TypeError' || error.message.includes('fetch')) {
return true;
}
// HTTP状态码判断
if (error.status && this.retryableStatuses.includes(error.status)) {
return true;
}
// 错误代码判断
if (error.code && this.retryableErrors.includes(error.code)) {
return true;
}
return false;
}
getDelay(attempt) {
const exponentialDelay = Math.min(
this.baseDelay * Math.pow(2, attempt),
30000 // 最大30秒
);
const jitter = Math.random() * 0.3 * exponentialDelay;
return exponentialDelay + jitter;
}
}
function ConditionalRetry() {
const [data, setData] = useState(null);
const [status, setStatus] = useState({ loading: true, error: null, attempt: 0 });
useEffect(() => {
const strategy = new RetryStrategy({
maxRetries: 3,
baseDelay: 1000,
});
const fetchWithStrategy = async (url, attempt = 0) => {
try {
const response = await fetch(url);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
throw error;
}
return await response.json();
} catch (error) {
if (strategy.shouldRetry(error, attempt)) {
const delay = strategy.getDelay(attempt);
setStatus({
loading: true,
error: null,
attempt: attempt + 1,
});
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithStrategy(url, attempt + 1);
}
throw error;
}
};
const fetchData = async () => {
try {
const result = await fetchWithStrategy('https://api.example.com/data');
setData(result);
setStatus({ loading: false, error: null, attempt: 0 });
} catch (error) {
setStatus({ loading: false, error: error.message, attempt: status.attempt });
}
};
fetchData();
}, []);
if (status.loading) {
return (
<div>
Loading...
{status.attempt > 0 && ` (重试 ${status.attempt}/${3})`}
</div>
);
}
if (status.error) {
return <div>Error: {status.error}</div>;
}
return <div>{JSON.stringify(data)}</div>;
}自定义Hook
useRetry Hook
jsx
import { useState, useCallback } from 'react';
function useRetry(maxRetries = 3, baseDelay = 1000) {
const [retryCount, setRetryCount] = useState(0);
const [isRetrying, setIsRetrying] = useState(false);
const executeWithRetry = useCallback(async (fn) => {
setRetryCount(0);
setIsRetrying(false);
for (let i = 0; i < maxRetries; i++) {
try {
const result = await fn();
setRetryCount(0);
setIsRetrying(false);
return result;
} catch (error) {
if (i === maxRetries - 1) {
setIsRetrying(false);
throw error;
}
const delay = baseDelay * Math.pow(2, i);
setRetryCount(i + 1);
setIsRetrying(true);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}, [maxRetries, baseDelay]);
return {
executeWithRetry,
retryCount,
isRetrying,
};
}
// 使用示例
function DataComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const { executeWithRetry, retryCount, isRetrying } = useRetry(3, 1000);
useEffect(() => {
const fetchData = async () => {
try {
const result = await executeWithRetry(async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('Failed');
return response.json();
});
setData(result);
} catch (error) {
setError(error.message);
}
};
fetchData();
}, [executeWithRetry]);
if (isRetrying) {
return <div>重试中... ({retryCount}/3)</div>;
}
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}useFetchWithRetry Hook
jsx
import { useState, useEffect } from 'react';
function useFetchWithRetry(url, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
retryOnStatus = [408, 429, 500, 502, 503, 504],
} = options;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [retryInfo, setRetryInfo] = useState({ count: 0, isRetrying: false });
useEffect(() => {
const controller = new AbortController();
let cancelled = false;
const fetchWithRetry = async (attempt = 0) => {
if (cancelled) return;
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
if (!response.ok) {
if (retryOnStatus.includes(response.status) && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
setRetryInfo({ count: attempt + 1, isRetrying: true });
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(attempt + 1);
}
throw new Error(`HTTP ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
setLoading(false);
setRetryInfo({ count: 0, isRetrying: false });
}
} catch (error) {
if (error.name === 'AbortError' || cancelled) return;
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
setRetryInfo({ count: attempt + 1, isRetrying: true });
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(attempt + 1);
}
if (!cancelled) {
setError(error);
setLoading(false);
setRetryInfo({ count: 0, isRetrying: false });
}
}
};
fetchWithRetry();
return () => {
cancelled = true;
controller.abort();
};
}, [url, JSON.stringify(options), maxRetries, baseDelay, retryOnStatus.join()]);
return { data, loading, error, retryInfo };
}
// 使用示例
function UserList() {
const { data, loading, error, retryInfo } = useFetchWithRetry(
'https://api.example.com/users',
{
maxRetries: 3,
baseDelay: 1000,
}
);
if (loading) {
return (
<div>
Loading...
{retryInfo.isRetrying && ` (重试 ${retryInfo.count}/3)`}
</div>
);
}
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}错误恢复
自动恢复
jsx
function AutoRecovery() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [recovering, setRecovering] = useState(false);
const fetchData = async () => {
try {
setRecovering(true);
setError(null);
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('Failed');
const json = await response.json();
setData(json);
setRecovering(false);
} catch (error) {
setError(error.message);
setRecovering(false);
// 5秒后自动重试
setTimeout(fetchData, 5000);
}
};
useEffect(() => {
fetchData();
}, []);
if (error) {
return (
<div className="error-recovery">
<p>Error: {error}</p>
{recovering ? (
<p>正在自动重试...</p>
) : (
<button onClick={fetchData}>手动重试</button>
)}
</div>
);
}
return <div>{JSON.stringify(data)}</div>;
}降级处理
jsx
function FallbackData() {
const [data, setData] = useState(null);
const [useFallback, setUseFallback] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Primary API failed');
}
const json = await response.json();
setData(json);
} catch (error) {
console.error('Primary API failed, using fallback');
try {
// 尝试备用API
const fallbackResponse = await fetch('https://backup-api.example.com/data');
const fallbackJson = await fallbackResponse.json();
setData(fallbackJson);
setUseFallback(true);
} catch (fallbackError) {
// 使用本地缓存数据
const cachedData = localStorage.getItem('cached-data');
if (cachedData) {
setData(JSON.parse(cachedData));
setUseFallback(true);
}
}
}
};
fetchData();
}, []);
return (
<div>
{useFallback && (
<div className="warning">使用降级数据</div>
)}
{JSON.stringify(data)}
</div>
);
}错误监控
错误日志
jsx
class ErrorLogger {
constructor() {
this.errors = [];
}
log(error, context = {}) {
const errorLog = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
context,
userAgent: navigator.userAgent,
url: window.location.href,
};
this.errors.push(errorLog);
// 发送到监控服务
this.sendToMonitoring(errorLog);
// 本地存储
this.saveToLocalStorage(errorLog);
}
sendToMonitoring(errorLog) {
// 发送到Sentry、LogRocket等
fetch('https://monitoring.example.com/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorLog),
}).catch(err => console.error('Failed to send error log:', err));
}
saveToLocalStorage(errorLog) {
try {
const logs = JSON.parse(localStorage.getItem('error-logs') || '[]');
logs.push(errorLog);
// 只保留最近100条
if (logs.length > 100) {
logs.shift();
}
localStorage.setItem('error-logs', JSON.stringify(logs));
} catch (err) {
console.error('Failed to save error log:', err);
}
}
getErrors() {
return this.errors;
}
}
const errorLogger = new ErrorLogger();
// 使用
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('Failed');
setData(await response.json());
} catch (error) {
errorLogger.log(error, {
component: 'DataComponent',
action: 'fetchData',
});
}
};
fetchData();
}, []);
return <div>{JSON.stringify(data)}</div>;
}总结
错误处理与重试要点:
- 错误类型:网络错误、HTTP错误、超时错误
- 错误边界:React Error Boundary捕获组件错误
- 重试机制:基础重试、指数退避、条件重试
- 自定义Hook:useRetry、useFetchWithRetry封装
- 错误恢复:自动恢复、降级处理、备用方案
- 错误监控:日志记录、监控服务集成
合理的错误处理和重试策略能够显著提升应用的稳定性和用户体验。