diff --git a/.changeset/xtjpqj-ah-fix.md b/.changeset/xtjpqj-ah-fix.md new file mode 100644 index 000000000..5e951644d --- /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..0d869ec66 100644 --- a/packages/react-db/src/useLiveSuspenseQuery.ts +++ b/packages/react-db/src/useLiveSuspenseQuery.ts @@ -193,9 +193,13 @@ 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`) {