Skip to main content
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')
}

3. Extract Query Key Parameters

// 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,
  })
}

Build docs developers (and LLMs) love