Skip to content

bug: infiniteQueryBehavior fetchPage rejects with undefined when cancelled instead of propagating AbortSignal reason #10475

@kuishou68

Description

@kuishou68

Bug Report

Describe the Bug

In packages/query-core/src/infiniteQueryBehavior.ts, the internal fetchPage helper function calls Promise.reject() without any argument (i.e., rejects with undefined) when the query has been cancelled via the AbortSignal:

const fetchPage = async (
  data: InfiniteData<unknown>,
  param: unknown,
  previous?: boolean,
): Promise<InfiniteData<unknown>> => {
  if (cancelled) {
    return Promise.reject()  // rejects with undefined, not a proper error
  }
  // ...
}

The cancelled flag is set to true by the AbortSignal abort event listener (via addConsumeAwareSignal). When abortController.abort() is called in query.ts (inside onCancel), the signal fires and cancelled becomes true. On the next fetchPage invocation within the multi-page fetch loop, the rejection propagates undefined as the error reason rather than a meaningful AbortError (i.e., context.signal.reason).

Impact

  1. Custom retry functions receive undefined as the error argument instead of an AbortError, breaking any logic that inspects the error type.
  2. onError / onSettled callbacks in QueryCache config receive undefined as the error.
  3. Error boundaries using throwOnError may receive undefined instead of a real error, leading to confusing error messages.

Steps to Reproduce

const client = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) => {
      console.log("error is:", error)          // logs: "error is: undefined"
      console.log(error instanceof DOMException) // false (should be true)
    }
  })
})

client.fetchInfiniteQuery({
  queryKey: ["items"],
  queryFn: async ({ pageParam }) => {
    await new Promise(r => setTimeout(r, 200))
    return { items: [pageParam], nextCursor: pageParam + 1 }
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  initialPageParam: 0,
  pages: 3,
})

// Cancel while fetching page 2 (after ~250ms, first page done)
setTimeout(() => client.cancelQueries({ queryKey: ["items"] }), 250)

Expected Behavior

Promise.reject() should propagate the actual abort reason:

if (cancelled) {
  return Promise.reject(context.signal.reason)
  // context.signal.reason is a DOMException{ name: "AbortError" }
  // when abortController.abort() is called without an explicit reason
}

This matches the Web standard: AbortSignal.reason is automatically set to a DOMException with name "AbortError" when AbortController.abort() is called without an argument.

Environment

  • @tanstack/query-core: v5.x (latest)
  • Affects: useInfiniteQuery, useSuspenseInfiniteQuery, prefetchInfiniteQuery

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions