Query functions are the core of how TanStack Query fetches data. They are async functions that return data or throw errors.
Query Function Basics
A query function is any function that returns a Promise:
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
throw new Error('Failed to fetch posts')
}
return await response.json()
},
})
}
Query Function Context
Query functions receive a QueryFunctionContext object as their first argument:
type QueryFunctionContext<TQueryKey extends QueryKey = QueryKey, TPageParam = never> = {
client: QueryClient // The QueryClient instance
queryKey: TQueryKey // The query key array
signal: AbortSignal // AbortSignal for cancellation
meta: QueryMeta | undefined // Optional metadata
pageParam?: unknown // For infinite queries
}
From types.ts:138-165, the full context definition:
export type QueryFunctionContext<
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
> = [TPageParam] extends [never]
? {
client: QueryClient
queryKey: TQueryKey
signal: AbortSignal
meta: QueryMeta | undefined
pageParam?: unknown
direction?: unknown
}
: {
client: QueryClient
queryKey: TQueryKey
signal: AbortSignal
pageParam: TPageParam
direction: FetchDirection
meta: QueryMeta | undefined
}
Using the Context
useQuery({
queryKey: ['post', postId],
queryFn: async ({ queryKey, signal }) => {
const [_key, postId] = queryKey
const response = await fetch(
`https://api.example.com/posts/${postId}`,
{ signal } // Pass signal for cancellation
)
return await response.json()
},
})
Return Values
Query functions must return a Promise that resolves to data:
// Async/await
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
return await response.json()
}
// Promise chain
queryFn: () => {
return fetch('https://api.example.com/posts')
.then(res => res.json())
}
// Direct Promise
queryFn: () => axios.get('/api/posts').then(res => res.data)
Data Validation
From query.ts:545-556, TanStack Query validates that data is not undefined:
const data = await this.#retryer.start()
if (data === undefined) {
if (process.env.NODE_ENV !== 'production') {
console.error(
`Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`,
)
}
throw new Error(`${this.queryHash} data is undefined`)
}
Query functions must not return undefined. If your API can return undefined, return null instead or wrap it in an object.
Error Handling
Throwing Errors
Throw errors to indicate failure:
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
Custom Error Objects
Throw custom error objects for better error handling:
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public response?: any
) {
super(message)
this.name = 'ApiError'
}
}
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
const errorData = await response.json()
throw new ApiError(
'Failed to fetch posts',
response.status,
errorData
)
}
return await response.json()
}
Error Dispatch
From query.ts:584-600, errors are dispatched to update query state:
this.#dispatch({
type: 'error',
error: error as TError,
})
// Notify cache callback
this.#cache.config.onError?.(
error as any,
this as Query<any, any, any, any>,
)
this.#cache.config.onSettled?.(
this.state.data,
error as any,
this as Query<any, any, any, any>,
)
throw error // rethrow the error for further handling
Request Cancellation
Using AbortSignal
The signal property in the context allows you to cancel requests:
queryFn: async ({ signal }) => {
const response = await fetch('https://api.example.com/posts', { signal })
return await response.json()
}
How Signal Works
From query.ts:430-443, the signal is lazily created:
const abortController = new AbortController()
// Adds an enumerable signal property to the object that
// sets abortSignalConsumed to true when the signal is read.
const addSignalProperty = (object: unknown) => {
Object.defineProperty(object, 'signal', {
enumerable: true,
get: () => {
this.#abortSignalConsumed = true
return abortController.signal
},
})
}
The signal is automatically aborted when:
- The query is cancelled
- The component unmounts (if the signal was consumed)
- A new fetch starts for the same query
TanStack Query only calls abort() on the signal if your query function accesses the signal property. This is an optimization to avoid unnecessary cancellations.
Manual Cancellation
You can manually cancel queries:
const query = useQuery({
queryKey: ['posts'],
queryFn: ({ signal }) => fetchPosts(signal),
})
// Cancel the query
await queryClient.cancelQueries({ queryKey: ['posts'] })
From queryClient.ts:278-291:
cancelQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
filters?: QueryFilters<TTaggedQueryKey>,
cancelOptions: CancelOptions = {},
): Promise<void> {
const defaultedCancelOptions = { revert: true, ...cancelOptions }
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.map((query) => query.cancel(defaultedCancelOptions)),
)
return Promise.all(promises).then(noop).catch(noop)
}
Query Function Types
Type Signature
From types.ts:96-100:
export type QueryFunction<
T = unknown,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
> = (context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>
Typed Query Functions
type Post = {
id: number
title: string
body: string
}
const fetchPost: QueryFunction<Post, ['post', number]> = async ({ queryKey }) => {
const [_key, postId] = queryKey
const response = await fetch(`https://api.example.com/posts/${postId}`)
return await response.json()
}
useQuery({
queryKey: ['post', 123],
queryFn: fetchPost,
})
Reusable Query Functions
Factory Pattern
function createFetcher<T>(url: string) {
return async ({ signal }: QueryFunctionContext): Promise<T> => {
const response = await fetch(url, { signal })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
}
useQuery({
queryKey: ['posts'],
queryFn: createFetcher<Post[]>('https://api.example.com/posts'),
})
Generic Fetcher
const apiFetcher = async <T>({
queryKey,
signal
}: QueryFunctionContext): Promise<T> => {
const [_key, ...params] = queryKey
const url = params.join('/')
const response = await fetch(`https://api.example.com/${url}`, { signal })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
useQuery({
queryKey: ['posts', 'list'],
queryFn: apiFetcher<Post[]>,
})
Query Function Best Practices
1. Always Handle Errors
// Good
queryFn: async () => {
const response = await fetch('/api/posts')
if (!response.ok) throw new Error('Failed to fetch')
return await response.json()
}
// Bad - doesn't check response.ok
queryFn: async () => {
const response = await fetch('/api/posts')
return await response.json() // Could be error HTML
}
2. Use Signal for Cancellation
// Good
queryFn: async ({ signal }) => {
return await fetch('/api/posts', { signal })
}
// Acceptable - if your library doesn't support signals
queryFn: async () => {
return await axios.get('/api/posts')
}
// Good
queryFn: ({ queryKey }) => {
const [_key, postId] = queryKey
return fetchPost(postId)
}
// Avoid - duplicating parameters
function usePost(postId: number) {
return useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId), // postId duplicated
})
}
4. Type Your Return Values
// Good - explicit return type
queryFn: async (): Promise<Post[]> => {
const response = await fetch('/api/posts')
return await response.json()
}
// Acceptable - inferred return type
queryFn: async () => {
const response = await fetch('/api/posts')
return (await response.json()) as Post[]
}
Skip Token
Use skipToken to skip query execution:
import { useQuery, skipToken } from '@tanstack/react-query'
function usePost(postId: number | undefined) {
return useQuery({
queryKey: ['post', postId],
queryFn: postId ? () => fetchPost(postId) : skipToken,
})
}
From queryClient.ts:616-618:
if (defaultedOptions.queryFn === skipToken) {
defaultedOptions.enabled = false
}
skipToken is a cleaner alternative to conditionally setting enabled: false when you don’t have a valid query function.
Query Function Context Usage
Real-world example from the basic example (examples/react/basic/src/index.tsx:84-89):
const getPostById = async (id: number): Promise<Post> => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
)
return await response.json()
}
function usePost(postId: number) {
return useQuery({
queryKey: ['post', postId],
queryFn: () => getPostById(postId),
enabled: !!postId,
})
}