Guides & Concepts

Query Functions

A query function can be any function that returns a promise. The promise should resolve data or throw an error.

ts
createQueryController(this, {
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

createQueryController(this, () => ({
  queryKey: ['todo', this.todoId],
  queryFn: () => fetchTodo(this.todoId),
}))
createQueryController(this, {
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

createQueryController(this, () => ({
  queryKey: ['todo', this.todoId],
  queryFn: () => fetchTodo(this.todoId),
}))

Handling Errors

TanStack Query needs failed query functions to throw or return a rejected promise. Some clients do that automatically. The browser fetch API does not, so check response.ok yourself:

ts
async function fetchTodos(): Promise<Todo[]> {
  const response = await fetch('/api/todos')

  if (!response.ok) {
    throw new Error('Failed to fetch todos')
  }

  return response.json() as Promise<Todo[]>
}
async function fetchTodos(): Promise<Todo[]> {
  const response = await fetch('/api/todos')

  if (!response.ok) {
    throw new Error('Failed to fetch todos')
  }

  return response.json() as Promise<Todo[]>
}

The thrown error is available on the query result:

ts
const query = this.todos()

if (query.isError) {
  return html`Error: ${query.error.message}`
}
const query = this.todos()

if (query.isError) {
  return html`Error: ${query.error.message}`
}

Query Function Context

TanStack Query passes a context object to every query function. It includes:

  • queryKey: the current query key
  • client: the QueryClient
  • signal: an AbortSignal for cancellation
  • meta: optional query metadata
ts
createQueryController(this, {
  queryKey: ['todos', { status: 'open' }],
  queryFn: async ({ queryKey, signal }) => {
    const [, filters] = queryKey
    const response = await fetch(`/api/todos?status=${filters.status}`, {
      signal,
    })
    if (!response.ok) throw new Error('Failed to fetch todos')
    return response.json() as Promise<Todo[]>
  },
})
createQueryController(this, {
  queryKey: ['todos', { status: 'open' }],
  queryFn: async ({ queryKey, signal }) => {
    const [, filters] = queryKey
    const response = await fetch(`/api/todos?status=${filters.status}`, {
      signal,
    })
    if (!response.ok) throw new Error('Failed to fetch todos')
    return response.json() as Promise<Todo[]>
  },
})

Infinite query functions also receive pageParam:

ts
createInfiniteQueryController(this, {
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjectsPage(pageParam),
  initialPageParam: 1,
  getNextPageParam: (lastPage) =>
    lastPage.hasMore ? lastPage.page + 1 : undefined,
})
createInfiniteQueryController(this, {
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjectsPage(pageParam),
  initialPageParam: 1,
  getNextPageParam: (lastPage) =>
    lastPage.hasMore ? lastPage.page + 1 : undefined,
})

See Infinite Queries for the controller-specific behavior.