-
Notifications
You must be signed in to change notification settings - Fork 107
Closed
Labels
Description
Background
TanStack Query provides robust error handling with retry logic, error callbacks, and error boundaries. While query-db-collection supports basic retry configuration, it doesn't fully expose Query's error handling capabilities. Currently, errors are logged to console and the collection is marked as ready, but the error details aren't easily accessible.
Current Behavior
From query.ts:407-415:
} else if (result.isError) {
console.error(
`[QueryCollection] Error observing query ${String(queryKey)}:`,
result.error
)
// Mark collection as ready even on error to avoid blocking apps
markReady()
}Problems
- Limited error visibility: Errors only go to console.error
- No error callbacks: Can't run custom logic on errors
- Missing error state in utils: No way to check last error
- No error boundary integration: Can't use React error boundaries
Proposed Solution
Enhance error handling to match TanStack Query's capabilities while respecting TanStack DB's design:
export interface QueryCollectionConfig<TItem, TError, TQueryKey> {
// ... existing options ...
/**
* Callback when query encounters an error (after all retries)
* @param error - The error that occurred
* @param query - The query instance
*/
onError?: (error: TError, query: Query) => void
/**
* Callback when query settles (success or error)
* @param data - The data if successful
* @param error - The error if failed
*/
onSettled?: (data?: Array<TItem>, error?: TError) => void
/**
* Whether errors should be thrown to nearest error boundary
* @default false
*/
useErrorBoundary?: boolean | ((error: TError, query: Query) => boolean)
}
export interface QueryCollectionUtils<TItem, TKey, TInsertInput> {
// ... existing utils ...
/**
* Get the last error encountered by the query (if any)
*/
lastError: () => TError | undefined
/**
* Check if the collection is in an error state
*/
isError: () => boolean
/**
* Get the number of consecutive sync failures
*/
errorCount: () => number
/**
* Clear the error state and retry
*/
clearError: () => Promise<void>
}Implementation Examples
Example 1: Error Notification
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
getKey: (item) => item.id,
// Show toast on error
onError: (error) => {
toast.error(`Failed to load todos: ${error.message}`)
},
// Log to monitoring service
onSettled: (data, error) => {
if (error) {
errorReporter.log(error, { collection: 'todos' })
}
},
queryClient,
})
)
// In component
const { data } = useLiveQuery((q) =>
q.from({ todo: todoCollection })
)
const error = todoCollection.utils.lastError()
if (error) {
return <ErrorMessage error={error} />
}Example 2: Error Boundary Integration
const criticalDataCollection = createCollection(
queryCollectionOptions({
queryKey: ['critical-data'],
queryFn: fetchCriticalData,
getKey: (item) => item.id,
// Throw to error boundary for critical failures
useErrorBoundary: (error) => {
// Only throw for non-network errors
return error.code \!== 'NETWORK_ERROR'
},
queryClient,
})
)
// In React component with error boundary
function App() {
return (
<ErrorBoundary fallback={<CriticalError />}>
<CriticalDataView />
</ErrorBoundary>
)
}Example 3: Offline-Aware Error Handling
const syncedCollection = createCollection(
queryCollectionOptions({
queryKey: ['synced-data'],
queryFn: fetchData,
getKey: (item) => item.id,
onError: (error, query) => {
// Check if we have stale data to show
const hasStaleData = syncedCollection.size > 0
if (navigator.onLine && \!hasStaleData) {
// Online but failed - serious error
toast.error('Failed to load data')
} else if (\!navigator.onLine && hasStaleData) {
// Offline but have cached data
toast.info('Showing offline data')
}
},
queryClient,
})
)
// 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 */}
</>
)
}Technical Implementation
Update the observer subscription to track errors:
// In queryCollectionOptions
let lastError: TError | undefined
let errorCount = 0
const actualUnsubscribeFn = localObserver.subscribe((result) => {
if (result.isSuccess) {
// Clear error state on success
lastError = undefined
errorCount = 0
// ... existing success logic ...
// Call onSettled
config.onSettled?.(result.data, undefined)
} else if (result.isError) {
lastError = result.error
errorCount++
// Call error callback
config.onError?.(result.error, localObserver.getCurrentQuery())
// Call onSettled
config.onSettled?.(undefined, result.error)
// Handle error boundary
if (config.useErrorBoundary) {
const shouldThrow = typeof config.useErrorBoundary === 'function'
? config.useErrorBoundary(result.error, localObserver.getCurrentQuery())
: config.useErrorBoundary
if (shouldThrow) {
throw result.error
}
}
// Still mark ready to avoid blocking
markReady()
}
})
// Add to utils
const utils = {
refetch,
...writeUtils,
lastError: () => lastError,
isError: () => \!\!lastError,
errorCount: () => errorCount,
clearError: async () => {
lastError = undefined
errorCount = 0
return refetch()
}
}Benefits
- Better error visibility: Errors accessible via utils, not just console
- Custom error handling: Run app-specific logic on errors
- Error boundary support: Integrate with React error boundaries
- Offline resilience: Build UIs that gracefully handle sync failures
- Monitoring integration: Easy to add error tracking
Design Considerations
- Data persistence: Even with errors, preserve cached data when available
- Don't clear data on error: Keep the collection state intact
- Progressive enhancement: All error features are optional
- Retry friendly:
clearError()utility makes retry UX simple
Testing Requirements
- Test onError callback is called with correct error
- Test onSettled is called for both success and error
- Test error boundary integration
- Test error state is accessible via utils functions
- Test clearError resets state and triggers refetch
- Test that collection still works with cached data despite errors