Skip to content

Commit d2c1423

Browse files
feat: add error tracking and retry methods to query collection utils (#441)
Co-authored-by: Kyle Mathews <[email protected]>
1 parent 92febbf commit d2c1423

File tree

4 files changed

+421
-6
lines changed

4 files changed

+421
-6
lines changed

.changeset/clear-days-raise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/query-db-collection": patch
3+
---
4+
5+
Add error tracking and retry methods to query collection utils.

docs/guides/error-handling.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,52 @@ The error includes:
4545
- `issues`: Array of validation issues with messages and paths
4646
- `message`: A formatted error message listing all issues
4747

48+
## Query Collection Error Tracking
49+
50+
Query collections provide enhanced error tracking utilities through the `utils` object. These methods expose error state information and provide recovery mechanisms for failed queries:
51+
52+
```tsx
53+
import { createCollection } from "@tanstack/db"
54+
import { queryCollectionOptions } from "@tanstack/query-db-collection"
55+
import { useLiveQuery } from "@tanstack/react-db"
56+
57+
const syncedCollection = createCollection(
58+
queryCollectionOptions({
59+
queryClient,
60+
queryKey: ['synced-data'],
61+
queryFn: fetchData,
62+
getKey: (item) => item.id,
63+
})
64+
)
65+
66+
// Component can check error state
67+
function DataList() {
68+
const { data } = useLiveQuery((q) => q.from({ item: syncedCollection }))
69+
const isError = syncedCollection.utils.isError()
70+
const errorCount = syncedCollection.utils.errorCount()
71+
72+
return (
73+
<>
74+
{isError && errorCount > 3 && (
75+
<Alert>
76+
Unable to sync. Showing cached data.
77+
<button onClick={() => syncedCollection.utils.clearError()}>
78+
Retry
79+
</button>
80+
</Alert>
81+
)}
82+
{/* Render data */}
83+
</>
84+
)
85+
}
86+
```
87+
88+
Error tracking methods:
89+
- **`lastError()`**: Returns the most recent error encountered by the query, or `undefined` if no errors have occurred:
90+
- **`isError()`**: Returns a boolean indicating whether the collection is currently in an error state:
91+
- **`errorCount()`**: Returns the number of consecutive sync failures. This counter is incremented only when queries fail completely (not per retry attempt) and is reset on successful queries:
92+
- **`clearError()`**: Clears the error state and triggers a refetch of the query. This method resets both `lastError` and `errorCount`:
93+
4894
## Collection Status and Error States
4995

5096
Collections track their status and transition between states:
@@ -281,6 +327,7 @@ When sync errors occur:
281327
- Error is logged to console: `[QueryCollection] Error observing query...`
282328
- Collection is marked as ready to prevent blocking the application
283329
- Cached data remains available
330+
- Error tracking counters are updated (`lastError`, `errorCount`)
284331

285332
### Sync Write Errors
286333

packages/query-db-collection/src/query.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,19 +301,21 @@ export interface QueryCollectionConfig<
301301
/**
302302
* Type for the refetch utility function
303303
*/
304-
export type RefetchFn = () => Promise<void>
304+
export type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>
305305

306306
/**
307307
* Utility methods available on Query Collections for direct writes and manual operations.
308308
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
309309
* @template TItem - The type of items stored in the collection
310310
* @template TKey - The type of the item keys
311311
* @template TInsertInput - The type accepted for insert operations
312+
* @template TError - The type of errors that can occur during queries
312313
*/
313314
export interface QueryCollectionUtils<
314315
TItem extends object = Record<string, unknown>,
315316
TKey extends string | number = string | number,
316317
TInsertInput extends object = TItem,
318+
TError = unknown,
317319
> extends UtilsRecord {
318320
/** Manually trigger a refetch of the query */
319321
refetch: RefetchFn
@@ -327,6 +329,21 @@ export interface QueryCollectionUtils<
327329
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
328330
/** Execute multiple write operations as a single atomic batch to the synced data store */
329331
writeBatch: (callback: () => void) => void
332+
/** Get the last error encountered by the query (if any); reset on success */
333+
lastError: () => TError | undefined
334+
/** Check if the collection is in an error state */
335+
isError: () => boolean
336+
/**
337+
* Get the number of consecutive sync failures.
338+
* Incremented only when query fails completely (not per retry attempt); reset on success.
339+
*/
340+
errorCount: () => number
341+
/**
342+
* Clear the error state and trigger a refetch of the query
343+
* @returns Promise that resolves when the refetch completes successfully
344+
* @throws Error if the refetch fails
345+
*/
346+
clearError: () => Promise<void>
330347
}
331348

332349
/**
@@ -424,7 +441,8 @@ export function queryCollectionOptions<
424441
utils: QueryCollectionUtils<
425442
ResolveType<TExplicit, TSchema, TQueryFn>,
426443
TKey,
427-
TInsertInput
444+
TInsertInput,
445+
TError
428446
>
429447
} {
430448
type TItem = ResolveType<TExplicit, TSchema, TQueryFn>
@@ -467,6 +485,13 @@ export function queryCollectionOptions<
467485
throw new GetKeyRequiredError()
468486
}
469487

488+
/** The last error encountered by the query */
489+
let lastError: TError | undefined
490+
/** The number of consecutive sync failures */
491+
let errorCount = 0
492+
/** The timestamp for when the query most recently returned the status as "error" */
493+
let lastErrorUpdatedAt = 0
494+
470495
const internalSync: SyncConfig<TItem>[`sync`] = (params) => {
471496
const { begin, write, commit, markReady, collection } = params
472497

@@ -500,6 +525,10 @@ export function queryCollectionOptions<
500525
type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]
501526
const handleUpdate: UpdateHandler = (result) => {
502527
if (result.isSuccess) {
528+
// Clear error state
529+
lastError = undefined
530+
errorCount = 0
531+
503532
const newItemsArray = result.data
504533

505534
if (
@@ -568,6 +597,12 @@ export function queryCollectionOptions<
568597
// Mark collection as ready after first successful query result
569598
markReady()
570599
} else if (result.isError) {
600+
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
601+
lastError = result.error
602+
errorCount++
603+
lastErrorUpdatedAt = result.errorUpdatedAt
604+
}
605+
571606
console.error(
572607
`[QueryCollection] Error observing query ${String(queryKey)}:`,
573608
result.error
@@ -595,10 +630,15 @@ export function queryCollectionOptions<
595630
* Refetch the query data
596631
* @returns Promise that resolves when the refetch is complete
597632
*/
598-
const refetch: RefetchFn = async (): Promise<void> => {
599-
return queryClient.refetchQueries({
600-
queryKey: queryKey,
601-
})
633+
const refetch: RefetchFn = (opts) => {
634+
return queryClient.refetchQueries(
635+
{
636+
queryKey: queryKey,
637+
},
638+
{
639+
throwOnError: opts?.throwOnError,
640+
}
641+
)
602642
}
603643

604644
// Create write context for manual write operations
@@ -689,6 +729,15 @@ export function queryCollectionOptions<
689729
utils: {
690730
refetch,
691731
...writeUtils,
732+
lastError: () => lastError,
733+
isError: () => !!lastError,
734+
errorCount: () => errorCount,
735+
clearError: () => {
736+
lastError = undefined
737+
errorCount = 0
738+
lastErrorUpdatedAt = 0
739+
return refetch({ throwOnError: true })
740+
},
692741
},
693742
}
694743
}

0 commit comments

Comments
 (0)