React Query v5 removed the onError, onSuccess, and onSettled callbacks from useQuery to improve predictability and align with React patterns. Developers must now use useEffect for component-level side effects or global QueryCache callbacks for centralized error handling.
This error occurs when upgrading from React Query v4 to v5 (TanStack Query v5) and attempting to use the `onError`, `onSuccess`, or `onSettled` callbacks within the `useQuery` hook. These callbacks were intentionally removed in v5 as part of a major breaking change to improve the predictability and maintainability of side effects in data-fetching logic. The removal addresses several fundamental issues with the previous callback-based approach: callbacks were tied to fetches rather than state changes (meaning they wouldn't fire when data came from cache), they could run multiple times per query (once per component using the hook), and they encouraged imperative patterns that conflicted with React's declarative philosophy. The v5 migration pushes developers toward more predictable alternatives like useEffect and error boundaries. It's important to note that these callbacks remain available in `useMutation` - only `useQuery` and `QueryObserver` were affected by this change.
Search your codebase for all instances of useQuery that use onError, onSuccess, or onSettled:
# Search for callback usage
grep -r "onError" --include="*.tsx" --include="*.ts" .
grep -r "onSuccess" --include="*.tsx" --include="*.ts" .
grep -r "onSettled" --include="*.tsx" --include="*.ts" .Make a list of all affected files to systematically migrate.
For component-specific side effects (like showing toast notifications), use useEffect to watch the query state:
Before (v4):
const { data, error } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
onError: (error) => {
toast.error(`Failed to load user: ${error.message}`);
},
onSuccess: (data) => {
console.log('User loaded:', data);
},
});After (v5):
const { data, error, isError, isSuccess } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
});
useEffect(() => {
if (isError && error) {
toast.error(`Failed to load user: ${error.message}`);
}
}, [isError, error]);
useEffect(() => {
if (isSuccess && data) {
console.log('User loaded:', data);
}
}, [isSuccess, data]);Important: Be aware that if you call this custom hook from multiple components, each will register its own effect and could trigger duplicate side effects (like multiple toast notifications).
For application-wide error handling (like logging or global notifications), use the global callbacks on QueryCache. These run once per query regardless of how many components use it:
import { QueryClient, QueryCache } from '@tanstack/react-query';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// This runs once per query, not once per component
console.error('Query error:', error);
// Check query meta for custom behavior
if (query.meta?.showErrorToast) {
toast.error(`Error: ${error.message}`);
}
},
onSuccess: (data, query) => {
// Global success handling
console.log('Query succeeded:', query.queryKey);
},
}),
});Then use query meta to opt-in specific queries:
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
meta: {
showErrorToast: true,
},
});This approach is fail-safe and ensures side effects run exactly once per query.
For errors that should affect the UI rendering, use React Error Boundaries with the throwOnError option:
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<h2>Something went wrong:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<UserProfile />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
function UserProfile() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
throwOnError: true, // Throw errors to nearest error boundary
});
return <div>{data.name}</div>;
}You can also use a function for conditional error throwing:
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
throwOnError: (error) => error.status >= 500, // Only throw server errors
});Note that useMutation still supports callbacks in v5. If you're using mutations, no changes are needed:
// This still works in v5
const mutation = useMutation({
mutationFn: updateUser,
onError: (error) => {
toast.error(error.message);
},
onSuccess: (data) => {
toast.success('User updated successfully');
},
});Only useQuery and QueryObserver callbacks were removed.
Why callbacks were removed:
The React Query team made this breaking change deliberately after extensive community discussion. The key issues with callbacks included:
1. Inconsistent behavior with caching: When queries returned cached data (with staleTime configured), callbacks wouldn't fire because they were tied to fetches, not state changes. This created confusion about when side effects would run.
2. Multiple invocations: Each component calling a custom hook with useQuery would trigger its own callback execution, leading to duplicate side effects (like showing multiple error notifications).
3. Closure problems: Callbacks could close over stale component state, leading to subtle bugs when the callback referenced outdated values.
4. React patterns misalignment: The imperative callback approach conflicted with React's declarative philosophy and the recommended patterns for handling side effects.
Performance considerations:
Using useEffect for error handling does introduce an additional render cycle - the query first updates with the error state, then the effect runs. For most applications this is negligible, but if you're concerned about performance, global QueryCache callbacks don't have this issue since they run outside the component render cycle.
Gradual migration strategy:
If you have a large codebase, consider creating a compatibility layer during migration:
// Temporary compatibility wrapper
function useQueryV4Compatible(options) {
const result = useQuery({
queryKey: options.queryKey,
queryFn: options.queryFn,
...options,
});
useEffect(() => {
if (options.onError && result.isError) {
options.onError(result.error);
}
}, [result.isError, result.error]);
return result;
}This allows gradual migration without breaking existing code, though you should ultimately remove this wrapper and use the recommended v5 patterns.
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