React Query cannot detect errors from fetch API calls because fetch does not automatically throw errors on HTTP error status codes. You must manually check response.ok and throw errors in your queryFn.
This error occurs when using TanStack Query (React Query) with the native fetch API. Unlike libraries like axios or graphql-request that automatically throw errors for unsuccessful HTTP calls, the fetch API does not throw errors by default for HTTP error status codes (4xx, 5xx). React Query requires query functions to either throw an error or return a rejected Promise to signal that something went wrong. When fetch returns a response with an error status code (like 404 or 500), it still resolves the Promise successfully with a Response object. This means React Query cannot detect the error and properly update the query state. The error message indicates that your queryFn completed successfully without throwing or rejecting, even though the HTTP request failed. This prevents React Query from triggering its error handling mechanisms and updating the error state properly.
The most common solution is to check the response.ok property and throw an error if it's false:
const { data, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
});This ensures React Query can properly detect and handle errors. The response.ok property is true for status codes in the 200-299 range.
For better error handling, include the status code and status text in your error:
const { data, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status} ${response.statusText}`
);
}
return response.json();
}
});This provides more context when debugging errors.
To avoid repeating error handling logic, create a wrapper function:
async function fetchWithErrorHandling(url: string, options?: RequestInit) {
const response = await fetch(url, options);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`HTTP ${response.status}: ${errorBody || response.statusText}`
);
}
return response.json();
}
// Use in queries
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchWithErrorHandling(`/api/users/${userId}`)
});This centralizes error handling and makes it reusable across all queries.
If your API returns error details in the response body, extract them:
const { data, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(
errorData?.message || `HTTP error ${response.status}`
);
}
return response.json();
}
});
// Access error message
if (isError) {
console.log(error.message); // Shows API error message
}This allows you to display server-provided error messages to users.
Instead of throwing, you can return a rejected Promise:
const { data, isError } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
return Promise.reject({
status: response.status,
message: response.statusText
});
}
return response.json();
}
});Both throw and Promise.reject() work equally well for signaling errors to React Query.
Re-throwing Caught Errors
If you catch errors during processing, you must re-throw them for React Query to detect them:
queryFn: async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Fetch failed');
return response.json();
} catch (error) {
// Log for debugging
console.error('Query failed:', error);
// Re-throw so React Query sees it
throw error;
}
}Custom Error Objects
You can throw custom error objects with additional data:
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
public data?: any
) {
super(`API Error: ${status} ${statusText}`);
this.name = 'ApiError';
}
}
queryFn: async () => {
const response = await fetch('/api/data');
if (!response.ok) {
const data = await response.json().catch(() => null);
throw new ApiError(response.status, response.statusText, data);
}
return response.json();
}Global Error Handling
Configure default error handling for all queries using QueryClient:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error instanceof ApiError && error.status < 500) {
return false;
}
return failureCount < 3;
},
onError: (error) => {
// Global error handler
console.error('Query error:', error);
toast.error('Something went wrong');
}
}
}
});Comparison with Axios
Axios automatically throws for error status codes, so this pattern isn't needed:
// With axios - no manual error checking needed
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => axios.get(`/api/users/${userId}`).then(res => res.data)
});Consider using axios or a similar library if you want automatic error handling across your application.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite