错误处理与重试策略
TanStack Query 的错误处理体系:单查询级、全局级、Error Boundary 集成,以及 retry / retryDelay 的配置策略。
#type / howto
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: 请求取消与 AbortController
- 关联: 网络状态感知
错误处理与重试策略
错误处理层级
queryFn 抛出异常
↓
retry 层(自动重试 N 次)
↓
单查询 onError 回调
↓
全局 QueryClient onError
↓
throwOnError → Error Boundary
单查询级错误处理
基本用法
const { data, error, isError } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
if (isError) {
return <ErrorMessage message={error.message} />;
}
onError 回调
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
onError: (error) => {
// v4 支持,v5 已移除单查询 onError
toast.error(`加载失败: ${error.message}`);
},
});
v5 变化:移除了单查询的 onError / onSuccess / onSettled,改用全局配置或 useEffect:
const { data, error, isError } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
useEffect(() => {
if (isError) {
toast.error(`加载失败: ${error.message}`);
}
}, [isError, error]);
全局错误处理
QueryClient 默认配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30_000),
},
},
});
全局 onError
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
// v4:所有 query 失败都会走这里
if (error instanceof Error) {
toast.error(error.message);
}
},
},
mutations: {
onError: (error) => {
toast.error(`操作失败: ${error.message}`);
},
},
},
});
v5 推荐方式:全局事件监听
// v5 用 QueryCache 的 onError
const queryCache = new QueryCache({
onError: (error, query) => {
// 可以按 queryKey 区分处理
if (query.queryKey[0] === 'user') {
toast.error('用户数据加载失败');
} else {
toast.error('请求失败');
}
// 401 → 跳登录
if (error.status === 401) {
authStore.logout();
}
},
});
const mutationCache = new MutationCache({
onError: (error) => {
toast.error(`操作失败: ${error.message}`);
},
});
const queryClient = new QueryClient({ queryCache, mutationCache });
重试策略
默认行为
// 默认:重试 3 次,指数退避
retry: 3
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30_000)
// 第 1 次重试:1 秒
// 第 2 次重试:2 秒
// 第 3 次重试:4 秒
按错误类型决定是否重试
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
retry: (failureCount, error) => {
// 4xx 客户端错误不重试
if (error.status >= 400 && error.status < 500) {
return false;
}
// 最多重试 3 次
return failureCount < 3;
},
});
全局配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// 401/403/404 不重试
if ([401, 403, 404].includes(error.status)) return false;
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30_000),
},
},
});
禁用重试
// 某些查询不需要重试
useQuery({
queryKey: ['realtime'],
queryFn: fetchRealtimeData,
retry: false,
});
throwOnError 与 Error Boundary
集成 React Error Boundary
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<UserProfile />
</ErrorBoundary>
);
}
function UserProfile() {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
throwOnError: true, // 错误会向上抛给 Error Boundary
});
return <div>{data.name}</div>;
}
条件 throwOnError
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
throwOnError: (error) => {
// 只有严重错误才抛给 Error Boundary
return error.status >= 500;
},
});
全局配置 throwOnError
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: (error) => error.status >= 500,
},
},
});
Mutation 错误处理
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
toast.success('创建成功');
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
// v5 中 mutation 仍支持 onError
toast.error(`创建失败: ${error.message}`);
},
});
带重试的 Mutation
const mutation = useMutation({
mutationFn: createUser,
retry: 2, // v5 支持 mutation 重试
retryDelay: 1000,
});
常见错误
区分网络错误和业务错误
// ❌ 所有错误都当网络错误处理
if (error) return <NetworkError />;
// ✅ 区分处理
if (error) {
if (error.status === 404) return <NotFound />;
if (error.status === 403) return <Forbidden />;
if (error.status >= 500) return <ServerError />;
return <GenericError message={error.message} />;
}
忽略 AbortError
// ❌ AbortError 被当作真正错误
if (error) toast.error(error.message);
// ✅ 忽略 AbortError
if (error && !(error instanceof DOMException && error.name === 'AbortError')) {
toast.error(error.message);
}
速查:错误处理策略
| 场景 | 策略 |
|---|---|
| 列表加载失败 | 显示错误提示 + 重试按钮 |
| 详情加载失败 | Error Boundary 或 inline 错误 |
| 401 未授权 | 全局拦截 → 跳登录页 |
| 404 未找到 | 显示 NotFound 组件 |
| 500 服务端错误 | Error Boundary + 重试 |
| Mutation 失败 | toast 提示 + 保留表单数据 |
| 网络离线 | [[tanstack-query-network-awareness |