Guides
Query functions
What a queryFn may return, how errors are reported, and what the query function context contains.
A query function can be literally any function that returns a value, a promise, or an Effect. The promise should either resolve the data or throw an error.
Unlike TanStack Query, undefined is a valid result: a queryFn typed Promise<User | undefined> caches undefined like any other value. So getData and a setData updater's previous are undefined both with no data and with undefined data; Query.getUnsafe(qc.getQuery(factory, input)).hasData tells them apart.
All of these are valid:
infiniteQueryOptions<{
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
}, number, unknown, never, readonly ["todos"], undefined>(options: Omit<InfiniteQueryOptions<{
error: unknown;
key: readonly ["todos"];
page: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
};
pageParam: number;
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): InfiniteQueryOptions<Flat<{
key: readonly ["todos"];
page: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
};
pageParam: number;
}>>
infiniteQueryOptions({
function getNextPageParam(lastPage: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
}, pages: readonly {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
}[], lastPageParam: number, pageParams: readonly number[]): number | null | undefined
getNextPageParam: (lastPage: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
}
lastPage: {
readonly items: readonly Todo[]items: interface ReadonlyArray<T>ReadonlyArray<Todo>;
readonly nextCursor: number | nullnextCursor: number | null;
}) => lastPage: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
}
lastPage.nextCursor: number | nullnextCursor,
initialPageParam: numberinitialPageParam: 0,
function queryFn(context: PageFunctionContext<number>): QueryFnResult<{
data: {
readonly items: ReadonlyArray<Todo>;
readonly nextCursor: number | null;
};
error: unknown;
services: never;
}>
queryFn: async () => ({ items: readonly Todo[]items: await function fetchAllTodos(): Promise<ReadonlyArray<Todo>>fetchAllTodos(), nextCursor: number | nullnextCursor: null }),
queryKey: readonly ["todos"]queryKey: ["todos"],
});
queryOptions<Todo, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Todo;
key: readonly ["todos", number];
}>>
queryOptions({ queryKey: readonly ["todos", number]queryKey: ["todos", const id: numberid], function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>
queryFn: () => function fetchTodoById(id: number): Promise<Todo>fetchTodoById(const id: numberid) });
queryOptions<Todo, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Todo;
key: readonly ["todos", number];
}>>
queryOptions({
queryKey: readonly ["todos", number]queryKey: ["todos", const id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>
queryFn: async () => {
const const data: Tododata = await function fetchTodoById(id: number): Promise<Todo>fetchTodoById(const id: numberid);
return const data: Tododata;
},
});
queryOptions<Todo, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: Todo;
key: readonly ["todos", number];
}>>
queryOptions({
queryKey: readonly ["todos", number]queryKey: ["todos", const id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
data: Todo;
error: unknown;
key: readonly ["todos", number];
services: never;
}>
queryFn: (context: QueryFunctionContextcontext) => function fetchTodoById(id: number): Promise<Todo>fetchTodoById(context: QueryFunctionContextcontext.QueryFunctionContext.queryKey: QueryKeyqueryKey[1] as number),
});
In practice the queryFn lives in a factory and closes over the factory's input, which is easier to type than reading it back from the key.
Handling and throwing errors
For raf-query to know a query failed, the query function must throw or return a rejected promise. The error is kept on the query: the first load's error rejects the promise useQuery returns, so use() throws it to the nearest error boundary, and a later background error shows up in controls.error while the old data stays readable.
queryOptions<unknown, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
data: unknown;
error: unknown;
key: readonly ["todos", number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: unknown;
key: readonly ["todos", number];
}>>
queryOptions({
queryKey: readonly ["todos", number]queryKey: ["todos", const id: numberid],
function queryFn(context: QueryFunctionContext): unknownqueryFn: async () => {
if (const somethingGoesWrong: booleansomethingGoesWrong) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Oh no!");
}
if (const somethingElseGoesWrong: booleansomethingElseGoesWrong) {
return var Promise: PromiseConstructorPromise.PromiseConstructor.reject<never>(reason?: any): Promise<never>reject(new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Oh no!"));
}
return const data: unknowndata;
},
});
Failed requests are retried three times before they count as failed.
With fetch and other clients that don't throw
Many clients, such as axios, throw for unsuccessful HTTP responses. fetch doesn't, so throw yourself:
queryOptions<any, unknown, never, readonly ["todos", number], undefined>(options: Omit<QueryOptions<{
data: any;
error: unknown;
key: readonly ["todos", number];
services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
data: any;
key: readonly ["todos", number];
}>>
queryOptions({
queryKey: readonly ["todos", number]queryKey: ["todos", const id: numberid],
function queryFn(context: QueryFunctionContext): anyqueryFn: async (context: QueryFunctionContextcontext) => {
const const response: Responseresponse = await function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>fetch(`/todos/${const id: numberid}`, { RequestInit.signal?: AbortSignal | null | undefinedsignal: context: QueryFunctionContextcontext.QueryFunctionContext.signal: AbortSignalsignal });
if (!const response: Responseresponse.Response.ok: booleanok) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error(`Request failed: ${const response: Responseresponse.Response.status: numberstatus}`);
}
return const response: Responseresponse.Body.json(): Promise<any>json();
},
});A not-found is often better as a value than an error: return null and let the page decide, or throw your router's notFound() from the queryFn so the route renders its not-found component.
The query function context
Each queryFn receives a context object:
queryKey: QueryKey, the query's keysignal: AbortSignal, aborted when the request is cancelled. Pass it tofetchso cancelled requests stop using the network.meta: QueryMeta | undefined, the query'smeta
Paged queries also get:
pageParam, the page to loaddirection: "forward" | "backward", which end of the list the page is added to
Redirects
In TanStack Start, a queryFn can throw redirect({ to: "/login" }). The router integration turns it into a navigation, from a loader or from a background refetch. Pass handleRedirects: false to setupRouterIntegration to handle them yourself.