Skip to content

Commit 4e1c433

Browse files
authored
fix(hydration): fix unhandled promise rejections (#9752)
1 parent ee85d16 commit 4e1c433

File tree

3 files changed

+35
-23
lines changed

3 files changed

+35
-23
lines changed

.changeset/tall-banks-drop.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tanstack/react-query': patch
3+
'@tanstack/query-core': patch
4+
---
5+
6+
Avoid unhandled promise rejection errors during de/rehydration of pending queries.

packages/query-core/src/hydration.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { tryResolveSync } from './thenable'
2+
import { noop } from './utils'
23
import type {
34
DefaultError,
45
MutationKey,
@@ -78,6 +79,26 @@ function dehydrateQuery(
7879
serializeData: TransformerFn,
7980
shouldRedactErrors: (error: unknown) => boolean,
8081
): DehydratedQuery {
82+
const promise = query.promise?.then(serializeData).catch((error) => {
83+
if (!shouldRedactErrors(error)) {
84+
// Reject original error if it should not be redacted
85+
return Promise.reject(error)
86+
}
87+
// If not in production, log original error before rejecting redacted error
88+
if (process.env.NODE_ENV !== 'production') {
89+
console.error(
90+
`A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
91+
)
92+
}
93+
return Promise.reject(new Error('redacted'))
94+
})
95+
96+
// Avoid unhandled promise rejections
97+
// We need the promise we dehydrate to reject to get the correct result into
98+
// the query cache, but we also want to avoid unhandled promise rejections
99+
// in whatever environment the prefetches are happening in.
100+
promise?.catch(noop)
101+
81102
return {
82103
dehydratedAt: Date.now(),
83104
state: {
@@ -89,19 +110,7 @@ function dehydrateQuery(
89110
queryKey: query.queryKey,
90111
queryHash: query.queryHash,
91112
...(query.state.status === 'pending' && {
92-
promise: query.promise?.then(serializeData).catch((error) => {
93-
if (!shouldRedactErrors(error)) {
94-
// Reject original error if it should not be redacted
95-
return Promise.reject(error)
96-
}
97-
// If not in production, log original error before rejecting redacted error
98-
if (process.env.NODE_ENV !== 'production') {
99-
console.error(
100-
`A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
101-
)
102-
}
103-
return Promise.reject(new Error('redacted'))
104-
}),
113+
promise,
105114
}),
106115
...(query.meta && { meta: query.meta }),
107116
}
@@ -259,10 +268,13 @@ export function hydrate(
259268
// which will re-use the passed `initialPromise`
260269
// Note that we need to call these even when data was synchronously
261270
// available, as we still need to set up the retryer
262-
void query.fetch(undefined, {
263-
// RSC transformed promises are not thenable
264-
initialPromise: Promise.resolve(promise).then(deserializeData),
265-
})
271+
query
272+
.fetch(undefined, {
273+
// RSC transformed promises are not thenable
274+
initialPromise: Promise.resolve(promise).then(deserializeData),
275+
})
276+
// Avoid unhandled promise rejections
277+
.catch(noop)
266278
}
267279
},
268280
)

packages/react-query/src/__tests__/HydrationBoundary.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,11 +451,6 @@ describe('React hydration', () => {
451451

452452
const dehydratedState = dehydrate(prefetchQueryClient)
453453

454-
function ignore() {
455-
// Ignore redacted unhandled rejection
456-
}
457-
process.addListener('unhandledRejection', ignore)
458-
459454
// Mimic what React/our synchronous thenable does for already rejected promises
460455
// @ts-expect-error
461456
dehydratedState.queries[0].promise.status = 'failure'
@@ -484,7 +479,6 @@ describe('React hydration', () => {
484479
await vi.advanceTimersByTimeAsync(21)
485480
expect(rendered.getByText('new')).toBeInTheDocument()
486481

487-
process.removeListener('unhandledRejection', ignore)
488482
hydrateSpy.mockRestore()
489483
prefetchQueryClient.clear()
490484
clientQueryClient.clear()

0 commit comments

Comments
 (0)