Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-days-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/query-db-collection": patch
---

Add error tracking and retry methods to query collection utils.
47 changes: 47 additions & 0 deletions docs/guides/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,52 @@ The error includes:
- `issues`: Array of validation issues with messages and paths
- `message`: A formatted error message listing all issues

## Query Collection Error Tracking

Query collections provide enhanced error tracking utilities through the `utils` object. These methods expose error state information and provide recovery mechanisms for failed queries:

```tsx
import { createCollection } from "@tanstack/db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { useLiveQuery } from "@tanstack/react-db"

const syncedCollection = createCollection(
queryCollectionOptions({
queryClient,
queryKey: ['synced-data'],
queryFn: fetchData,
getKey: (item) => item.id,
})
)

// Component can check error state
function DataList() {
const { data } = useLiveQuery((q) => q.from({ item: syncedCollection }))
const isError = syncedCollection.utils.isError()
const errorCount = syncedCollection.utils.errorCount()

return (
<>
{isError && errorCount > 3 && (
<Alert>
Unable to sync. Showing cached data.
<button onClick={() => syncedCollection.utils.clearError()}>
Retry
</button>
</Alert>
)}
{/* Render data */}
</>
)
}
```

Error tracking methods:
- **`lastError()`**: Returns the most recent error encountered by the query, or `undefined` if no errors have occurred:
- **`isError()`**: Returns a boolean indicating whether the collection is currently in an error state:
- **`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:
- **`clearError()`**: Clears the error state and triggers a refetch of the query. This method resets both `lastError` and `errorCount`:

## Collection Status and Error States

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

### Sync Write Errors

Expand Down
61 changes: 55 additions & 6 deletions packages/query-db-collection/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,21 @@ export interface QueryCollectionConfig<
/**
* Type for the refetch utility function
*/
export type RefetchFn = () => Promise<void>
export type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>

/**
* Utility methods available on Query Collections for direct writes and manual operations.
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
* @template TItem - The type of items stored in the collection
* @template TKey - The type of the item keys
* @template TInsertInput - The type accepted for insert operations
* @template TError - The type of errors that can occur during queries
*/
export interface QueryCollectionUtils<
TItem extends object = Record<string, unknown>,
TKey extends string | number = string | number,
TInsertInput extends object = TItem,
TError = unknown,
> extends UtilsRecord {
/** Manually trigger a refetch of the query */
refetch: RefetchFn
Expand All @@ -327,6 +329,21 @@ export interface QueryCollectionUtils<
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
/** Execute multiple write operations as a single atomic batch to the synced data store */
writeBatch: (callback: () => void) => void
/** Get the last error encountered by the query (if any); reset on success */
lastError: () => TError | undefined
/** Check if the collection is in an error state */
isError: () => boolean
/**
* Get the number of consecutive sync failures.
* Incremented only when query fails completely (not per retry attempt); reset on success.
*/
errorCount: () => number
/**
* Clear the error state and trigger a refetch of the query
* @returns Promise that resolves when the refetch completes successfully
* @throws Error if the refetch fails
*/
clearError: () => Promise<void>
}

/**
Expand Down Expand Up @@ -424,7 +441,8 @@ export function queryCollectionOptions<
utils: QueryCollectionUtils<
ResolveType<TExplicit, TSchema, TQueryFn>,
TKey,
TInsertInput
TInsertInput,
TError
>
} {
type TItem = ResolveType<TExplicit, TSchema, TQueryFn>
Expand Down Expand Up @@ -467,6 +485,13 @@ export function queryCollectionOptions<
throw new GetKeyRequiredError()
}

/** The last error encountered by the query */
let lastError: TError | undefined
/** The number of consecutive sync failures */
let errorCount = 0
/** The timestamp for when the query most recently returned the status as "error" */
let lastErrorUpdatedAt = 0

const internalSync: SyncConfig<TItem>[`sync`] = (params) => {
const { begin, write, commit, markReady, collection } = params

Expand Down Expand Up @@ -500,6 +525,10 @@ export function queryCollectionOptions<
type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]
const handleUpdate: UpdateHandler = (result) => {
if (result.isSuccess) {
// Clear error state
lastError = undefined
errorCount = 0

const newItemsArray = result.data

if (
Expand Down Expand Up @@ -568,6 +597,12 @@ export function queryCollectionOptions<
// Mark collection as ready after first successful query result
markReady()
} else if (result.isError) {
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
lastError = result.error
errorCount++
lastErrorUpdatedAt = result.errorUpdatedAt
}

console.error(
`[QueryCollection] Error observing query ${String(queryKey)}:`,
result.error
Expand Down Expand Up @@ -595,10 +630,15 @@ export function queryCollectionOptions<
* Refetch the query data
* @returns Promise that resolves when the refetch is complete
*/
const refetch: RefetchFn = async (): Promise<void> => {
return queryClient.refetchQueries({
queryKey: queryKey,
})
const refetch: RefetchFn = (opts) => {
return queryClient.refetchQueries(
{
queryKey: queryKey,
},
{
throwOnError: opts?.throwOnError,
}
)
}

// Create write context for manual write operations
Expand Down Expand Up @@ -689,6 +729,15 @@ export function queryCollectionOptions<
utils: {
refetch,
...writeUtils,
lastError: () => lastError,
isError: () => !!lastError,
errorCount: () => errorCount,
clearError: () => {
lastError = undefined
errorCount = 0
lastErrorUpdatedAt = 0
return refetch({ throwOnError: true })
},
},
}
}
Loading
Loading