diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index 69e1448327..ff73335a0a 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -224,6 +224,8 @@ queryClient.setQueryData(queryKey, updater)
 setQueryData(queryKey, newData)
 ```
 
+If the value is `undefined`, the query data is not updated.
+
 **Using an updater function**
 
 For convenience in syntax, you can also pass an updater function which receives the current data value and returns the new one:
@@ -232,6 +234,8 @@ For convenience in syntax, you can also pass an updater function which receives
 setQueryData(queryKey, oldData => newData)
 ```
 
+If the updater function returns `undefined`, the query data will not be updated.
+
 ## `queryClient.getQueryState`
 
 `getQueryState` is a synchronous function that can be used to get an existing query's state. If the query does not exist, `undefined` will be returned.
diff --git a/docs/src/pages/reference/useQuery.md b/docs/src/pages/reference/useQuery.md
index 4f628bebbc..e826bed046 100644
--- a/docs/src/pages/reference/useQuery.md
+++ b/docs/src/pages/reference/useQuery.md
@@ -68,7 +68,7 @@ const result = useQuery({
 
 **Options**
 
-- `queryKey:  unknown[]`
+- `queryKey: unknown[]`
   - **Required**
   - The query key to use for this query.
   - The query key will be hashed into a stable hash. See [Query Keys](../guides/query-keys) for more information.
@@ -77,7 +77,7 @@ const result = useQuery({
   - **Required, but only if no default query function has been defined** See [Default Query Function](../guides/default-query-function) for more information.
   - The function that the query will use to request data.
   - Receives a [QueryFunctionContext](../guides/query-functions#queryfunctioncontext)
-  - Must return a promise that will either resolve data or throw an error.
+  - Must return a promise that will either resolve data or throw an error. The data cannot be `undefined`.
 - `enabled: boolean`
   - Set this to `false` to disable this query from automatically running.
   - Can be used for [Dependent Queries](../guides/dependent-queries).
diff --git a/src/core/query.ts b/src/core/query.ts
index 06935088dc..be087dbbea 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -1,7 +1,5 @@
 import {
   getAbortController,
-  Updater,
-  functionalUpdate,
   noop,
   replaceEqualDeep,
   timeUntilStale,
@@ -195,14 +193,11 @@ export class Query<
   }
 
   setData(
-    updater: Updater<TData | undefined, TData>,
+    data: TData,
     options?: SetDataOptions & { notifySuccess: boolean }
   ): TData {
     const prevData = this.state.data
 
-    // Get the new data
-    let data = functionalUpdate(updater, prevData)
-
     // Use prev data if an isDataEqual function is defined and returns `true`
     if (this.options.isDataEqual?.(prevData, data)) {
       data = prevData as TData
@@ -438,11 +433,41 @@ export class Query<
       this.dispatch({ type: 'fetch', meta: context.fetchOptions?.meta })
     }
 
+    const onError = (error: TError | { silent?: boolean }) => {
+      // Optimistically update state if needed
+      if (!(isCancelledError(error) && error.silent)) {
+        this.dispatch({
+          type: 'error',
+          error: error as TError,
+        })
+      }
+
+      if (!isCancelledError(error)) {
+        // Notify cache callback
+        this.cache.config.onError?.(error, this as Query<any, any, any, any>)
+
+        if (process.env.NODE_ENV !== 'production') {
+          getLogger().error(error)
+        }
+      }
+
+      if (!this.isFetchingOptimistic) {
+        // Schedule query gc after fetching
+        this.scheduleGc()
+      }
+      this.isFetchingOptimistic = false
+    }
+
     // Try to fetch the data
     this.retryer = createRetryer({
       fn: context.fetchFn as () => TData,
       abort: abortController?.abort?.bind(abortController),
       onSuccess: data => {
+        if (typeof data === 'undefined') {
+          onError(new Error('Query data cannot be undefined') as any)
+          return
+        }
+
         this.setData(data as TData)
 
         // Notify cache callback
@@ -454,30 +479,7 @@ export class Query<
         }
         this.isFetchingOptimistic = false
       },
-      onError: (error: TError | { silent?: boolean }) => {
-        // Optimistically update state if needed
-        if (!(isCancelledError(error) && error.silent)) {
-          this.dispatch({
-            type: 'error',
-            error: error as TError,
-          })
-        }
-
-        if (!isCancelledError(error)) {
-          // Notify cache callback
-          this.cache.config.onError?.(error, this as Query<any, any, any, any>)
-
-          if (process.env.NODE_ENV !== 'production') {
-            getLogger().error(error)
-          }
-        }
-
-        if (!this.isFetchingOptimistic) {
-          // Schedule query gc after fetching
-          this.scheduleGc()
-        }
-        this.isFetchingOptimistic = false
-      },
+      onError,
       onFail: () => {
         this.dispatch({ type: 'failed' })
       },
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index d09461ef83..589b723504 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -8,6 +8,7 @@ import {
   partialMatchKey,
   hashQueryKeyByOptions,
   MutationFilters,
+  functionalUpdate,
 } from './utils'
 import type {
   QueryClientConfig,
@@ -125,14 +126,22 @@ export class QueryClient {
 
   setQueryData<TData>(
     queryKey: QueryKey,
-    updater: Updater<TData | undefined, TData>,
+    updater: Updater<TData | undefined, TData> | undefined,
     options?: SetDataOptions
-  ): TData {
+  ): TData | undefined {
+    const query = this.queryCache.find<TData>(queryKey)
+    const prevData = query?.state.data
+    const data = functionalUpdate(updater, prevData)
+
+    if (typeof data === 'undefined') {
+      return undefined
+    }
+
     const parsedOptions = parseQueryArgs(queryKey)
     const defaultedOptions = this.defaultQueryOptions(parsedOptions)
     return this.queryCache
       .build(this, defaultedOptions)
-      .setData(updater, { ...options, notifySuccess: false })
+      .setData(data, { ...options, notifySuccess: false })
   }
 
   setQueriesData<TData>(
@@ -151,7 +160,7 @@ export class QueryClient {
     queryKeyOrFilters: QueryKey | QueryFilters,
     updater: Updater<TData | undefined, TData>,
     options?: SetDataOptions
-  ): [QueryKey, TData][] {
+  ): [QueryKey, TData | undefined][] {
     return notifyManager.batch(() =>
       this.getQueryCache()
         .findAll(queryKeyOrFilters)
diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx
index cc120b933d..b5aefa4b14 100644
--- a/src/core/tests/query.test.tsx
+++ b/src/core/tests/query.test.tsx
@@ -12,6 +12,7 @@ import {
   isError,
   onlineManager,
   QueryFunctionContext,
+  QueryObserverResult,
 } from '../..'
 import { waitFor } from '@testing-library/react'
 
@@ -787,6 +788,7 @@ describe('query', () => {
     let signalTest: any
     await queryClient.prefetchQuery(key, ({ signal }) => {
       signalTest = signal
+      return 'data'
     })
 
     expect(signalTest).toBeUndefined()
@@ -814,6 +816,31 @@ describe('query', () => {
     consoleMock.mockRestore()
   })
 
+  test('fetch should dispatch an error if the queryFn returns undefined', async () => {
+    const key = queryKey()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: (() => undefined) as any,
+      retry: false,
+    })
+
+    let observerResult: QueryObserverResult<unknown, unknown> | undefined
+
+    const unsubscribe = observer.subscribe(result => {
+      observerResult = result
+    })
+
+    await sleep(10)
+
+    expect(observerResult).toMatchObject({
+      isError: true,
+      error: new Error('Query data cannot be undefined'),
+    })
+
+    unsubscribe()
+  })
+
   test('fetch should dispatch fetch if is fetching and current promise is undefined', async () => {
     const key = queryKey()
 
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index c32cf97cc0..0d60ff4206 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -53,7 +53,7 @@ describe('queryClient', () => {
         },
       })
 
-      const fetchData = () => Promise.resolve(undefined)
+      const fetchData = () => Promise.resolve('data')
       await testClient.prefetchQuery(key, fetchData)
       const newQuery = testClient.getQueryCache().find(key)
       expect(newQuery?.options.cacheTime).toBe(Infinity)
@@ -301,6 +301,34 @@ describe('queryClient', () => {
       expect(queryClient.getQueryData(key)).toBe('qux')
     })
 
+    test('should not create a new query if query was not found and data is undefined', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+      queryClient.setQueryData(key, undefined)
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+    })
+
+    test('should not create a new query if query was not found and updater returns undefined', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+      queryClient.setQueryData(key, () => undefined)
+      expect(queryClient.getQueryCache().find(key)).toBe(undefined)
+    })
+
+    test('should not update query data if data is undefined', () => {
+      const key = queryKey()
+      queryClient.setQueryData(key, 'qux')
+      queryClient.setQueryData(key, undefined)
+      expect(queryClient.getQueryData(key)).toBe('qux')
+    })
+
+    test('should not update query data if updater returns undefined', () => {
+      const key = queryKey()
+      queryClient.setQueryData(key, 'qux')
+      queryClient.setQueryData(key, () => undefined)
+      expect(queryClient.getQueryData(key)).toBe('qux')
+    })
+
     test('should accept an update function', () => {
       const key = queryKey()
 
@@ -871,8 +899,8 @@ describe('queryClient', () => {
     test('should refetch all queries when no arguments are given', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer1 = new QueryObserver(queryClient, {
@@ -897,8 +925,8 @@ describe('queryClient', () => {
     test('should be able to refetch all fresh queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -916,8 +944,8 @@ describe('queryClient', () => {
     test('should be able to refetch all stale queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -936,8 +964,8 @@ describe('queryClient', () => {
     test('should be able to refetch all stale and active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       queryClient.invalidateQueries(key1)
@@ -958,8 +986,8 @@ describe('queryClient', () => {
     test('should be able to refetch all active and inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -977,8 +1005,8 @@ describe('queryClient', () => {
     test('should be able to refetch all active and inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -996,8 +1024,8 @@ describe('queryClient', () => {
     test('should be able to refetch only active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1015,8 +1043,8 @@ describe('queryClient', () => {
     test('should be able to refetch only inactive queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1060,8 +1088,8 @@ describe('queryClient', () => {
     test('should refetch active queries by default', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1079,8 +1107,8 @@ describe('queryClient', () => {
     test('should not refetch inactive queries by default', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1098,8 +1126,8 @@ describe('queryClient', () => {
     test('should not refetch active queries when "refetch" is "none"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1119,8 +1147,8 @@ describe('queryClient', () => {
     test('should refetch inactive queries when "refetch" is "inactive"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1142,8 +1170,8 @@ describe('queryClient', () => {
     test('should refetch active and inactive queries when "refetch" is "all"', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       await queryClient.fetchQuery(key1, queryFn1)
       await queryClient.fetchQuery(key2, queryFn2)
       const observer = new QueryObserver(queryClient, {
@@ -1269,8 +1297,8 @@ describe('queryClient', () => {
     test('should refetch all active queries', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
-      const queryFn1 = jest.fn()
-      const queryFn2 = jest.fn()
+      const queryFn1 = jest.fn().mockReturnValue('data1')
+      const queryFn2 = jest.fn().mockReturnValue('data2')
       const observer1 = new QueryObserver(queryClient, {
         queryKey: key1,
         queryFn: queryFn1,
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index 9daf7d8207..ed95023aaf 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -331,7 +331,7 @@ describe('queryObserver', () => {
 
   test('should be able to watch a query without defining a query function', async () => {
     const key = queryKey()
-    const queryFn = jest.fn()
+    const queryFn = jest.fn().mockReturnValue('data')
     const callback = jest.fn()
     const observer = new QueryObserver(queryClient, {
       queryKey: key,
@@ -346,7 +346,7 @@ describe('queryObserver', () => {
 
   test('should accept unresolved query config in update function', async () => {
     const key = queryKey()
-    const queryFn = jest.fn()
+    const queryFn = jest.fn().mockReturnValue('data')
     const observer = new QueryObserver(queryClient, {
       queryKey: key,
       enabled: false,
diff --git a/src/core/types.ts b/src/core/types.ts
index f177030963..9df4e6a954 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -6,10 +6,14 @@ import type { QueryCache } from './queryCache'
 import type { MutationCache } from './mutationCache'
 
 export type QueryKey = readonly unknown[]
+export type QueryFunctionData<T> = T extends undefined ? never : T
+
 export type QueryFunction<
   T = unknown,
   TQueryKey extends QueryKey = QueryKey
-> = (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>
+> = (
+  context: QueryFunctionContext<TQueryKey>
+) => QueryFunctionData<T> | Promise<QueryFunctionData<T>>
 
 export interface QueryFunctionContext<
   TQueryKey extends QueryKey = QueryKey,
diff --git a/src/reactjs/tests/ssr.test.tsx b/src/reactjs/tests/ssr.test.tsx
index 2363a160c0..1a2c738331 100644
--- a/src/reactjs/tests/ssr.test.tsx
+++ b/src/reactjs/tests/ssr.test.tsx
@@ -60,7 +60,10 @@ describe('Server Side Rendering', () => {
     const queryCache = new QueryCache()
     const queryClient = new QueryClient({ queryCache })
     const key = queryKey()
-    const queryFn = jest.fn(() => sleep(10))
+    const queryFn = jest.fn(() => {
+      sleep(10)
+      return 'data'
+    })
 
     function Page() {
       const query = useQuery(key, queryFn)
diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx
index a4b68d4210..6bf53b56ea 100644
--- a/src/reactjs/tests/suspense.test.tsx
+++ b/src/reactjs/tests/suspense.test.tsx
@@ -114,7 +114,10 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     const queryFn = jest.fn()
-    queryFn.mockImplementation(() => sleep(10))
+    queryFn.mockImplementation(() => {
+      sleep(10)
+      return 'data'
+    })
 
     function Page() {
       useQuery([key], queryFn, { suspense: true })
@@ -138,7 +141,14 @@ describe("useQuery's in Suspense mode", () => {
     const key = queryKey()
 
     function Page() {
-      useQuery(key, () => sleep(10), { suspense: true })
+      useQuery(
+        key,
+        () => {
+          sleep(10)
+          return 'data'
+        },
+        { suspense: true }
+      )
 
       return <>rendered</>
     }
@@ -212,10 +222,17 @@ describe("useQuery's in Suspense mode", () => {
     const successFn2 = jest.fn()
 
     function FirstComponent() {
-      useQuery(key, () => sleep(10), {
-        suspense: true,
-        onSuccess: successFn1,
-      })
+      useQuery(
+        key,
+        () => {
+          sleep(10)
+          return 'data'
+        },
+        {
+          suspense: true,
+          onSuccess: successFn1,
+        }
+      )
 
       return <span>first</span>
     }
diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx
index 93e1ea3e52..6a68bd126d 100644
--- a/src/reactjs/tests/useQuery.test.tsx
+++ b/src/reactjs/tests/useQuery.test.tsx
@@ -3447,7 +3447,7 @@ describe('useQuery', () => {
       const [enabled, setEnabled] = React.useState(false)
       const [isPrefetched, setPrefetched] = React.useState(false)
 
-      const query = useQuery(key, () => undefined, {
+      const query = useQuery(key, () => 'data', {
         enabled,
       })
 
@@ -3609,7 +3609,7 @@ describe('useQuery', () => {
     const key = queryKey()
 
     function Page() {
-      const query = useQuery(key, () => undefined, {
+      const query = useQuery(key, () => 'data', {
         enabled: false,
       })