From 3d84aecb16c4421bf315638c366ff7eb0a21ff89 Mon Sep 17 00:00:00 2001 From: Dmitrii Troitskii Date: Mon, 9 Mar 2026 18:34:58 +0000 Subject: [PATCH 1/2] fix(db): reject preload() promise when collection errors, propagate to ErrorBoundary When a collection transitions to the 'error' state while preload() is pending, the promise now rejects instead of hanging forever. This allows useLiveSuspenseQuery to throw the error during render so it reaches the nearest . Also re-throws the actual error from collection.utils?.lastError in useLiveSuspenseQuery instead of a generic fallback message. Fixes #1343 --- .changeset/xtjpqj-ah-fix.md | 19 +++++++++++++++++++ packages/db/src/collection/sync.ts | 12 ++++++++++++ packages/react-db/src/useLiveSuspenseQuery.ts | 12 +++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 .changeset/xtjpqj-ah-fix.md diff --git a/.changeset/xtjpqj-ah-fix.md b/.changeset/xtjpqj-ah-fix.md new file mode 100644 index 000000000..281b31b5b --- /dev/null +++ b/.changeset/xtjpqj-ah-fix.md @@ -0,0 +1,19 @@ +--- +"@tanstack/db": patch +"@tanstack/react-db": patch +--- + +fix(db): reject preload() promise when collection transitions to error state + +Previously, `preload()` only resolved when the collection became ready. If +the collection transitioned to the `error` state while the promise was pending +(e.g. because the `queryFn` threw), the promise would hang forever, keeping +any `` boundary suspended indefinitely and preventing the error from +reaching an ``. + +Now `preload()` subscribes to `status:change` events and rejects the promise +when the collection enters the `error` state. `useLiveSuspenseQuery` is also +updated to re-throw the actual error from `collection.utils?.lastError` instead +of a generic fallback message, so `` receives the original error. + +Fixes #1343 diff --git a/packages/db/src/collection/sync.ts b/packages/db/src/collection/sync.ts index 4b71e4afd..844c606d3 100644 --- a/packages/db/src/collection/sync.ts +++ b/packages/db/src/collection/sync.ts @@ -271,8 +271,20 @@ export class CollectionSyncManager< return } + // Subscribe to status changes to reject the promise when collection errors. + // This is necessary because onFirstReady only fires on success, but if the + // collection transitions to 'error' while the promise is pending it would + // hang forever and keep the Suspense boundary suspended indefinitely. + const unsubscribeError = this._events.on(`status:change`, (event) => { + if (event.status === `error`) { + unsubscribeError() + reject(new CollectionIsInErrorStateError()) + } + }) + // Register callback BEFORE starting sync to avoid race condition this.lifecycle.onFirstReady(() => { + unsubscribeError() resolve() }) diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index c35e7e11a..a29d74914 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -193,9 +193,15 @@ export function useLiveSuspenseQuery( // After success, errors surface as stale data (matches TanStack Query behavior) if (result.status === `error` && !hasBeenReadyRef.current) { promiseRef.current = null - // TODO: Once collections hold a reference to their last error object (#671), - // we should rethrow that actual error instead of creating a generic message - throw new Error(`Collection "${result.collection.id}" failed to load`) + // Re-throw the actual error from the collection if available (e.g. from + // query-db-collection's utils.lastError), otherwise fall back to a generic + // message. Throwing here propagates the error to the nearest ErrorBoundary. + const lastError = (result.collection as any).utils?.lastError + throw lastError instanceof Error + ? lastError + : new Error( + `Collection "${result.collection.id}" failed to load`, + ) } if (result.status === `loading` || result.status === `idle`) { From 673d99b72f1b2b05f9a525168ca2e6e0cf9c2496 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:36:16 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .changeset/xtjpqj-ah-fix.md | 4 ++-- packages/react-db/src/useLiveSuspenseQuery.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.changeset/xtjpqj-ah-fix.md b/.changeset/xtjpqj-ah-fix.md index 281b31b5b..5e951644d 100644 --- a/.changeset/xtjpqj-ah-fix.md +++ b/.changeset/xtjpqj-ah-fix.md @@ -1,6 +1,6 @@ --- -"@tanstack/db": patch -"@tanstack/react-db": patch +'@tanstack/db': patch +'@tanstack/react-db': patch --- fix(db): reject preload() promise when collection transitions to error state diff --git a/packages/react-db/src/useLiveSuspenseQuery.ts b/packages/react-db/src/useLiveSuspenseQuery.ts index a29d74914..0d869ec66 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -199,9 +199,7 @@ export function useLiveSuspenseQuery( const lastError = (result.collection as any).utils?.lastError throw lastError instanceof Error ? lastError - : new Error( - `Collection "${result.collection.id}" failed to load`, - ) + : new Error(`Collection "${result.collection.id}" failed to load`) } if (result.status === `loading` || result.status === `idle`) {