Error Handling
此内容尚不支持你的语言。
Error Handling
Section titled “Error Handling”TanStack DB provides comprehensive error handling capabilities to ensure robust data synchronization and state management. This guide covers the built-in error handling mechanisms and how to work with them effectively.
Error Types
Section titled “Error Types”TanStack DB provides named error classes for better error handling and type safety. All error classes can be imported from @tanstack/db (or more commonly, the framework-specific package e.g. @tanstack/react-db):
import { SchemaValidationError, CollectionInErrorStateError, DuplicateKeyError, MissingHandlerError, TransactionError, // ... and many more} from "@tanstack/db"SchemaValidationError
Section titled “SchemaValidationError”Thrown when data doesn’t match the collection’s schema during insert or update operations:
import { SchemaValidationError } from "@tanstack/db"
try { todoCollection.insert({ text: 123 }) // Invalid type} catch (error) { if (error instanceof SchemaValidationError) { console.log(error.type) // 'insert' or 'update' console.log(error.issues) // Array of validation issues // Example issue: { message: "Expected string, received number", path: ["text"] } }}The error includes:
type: Whether it was an ‘insert’ or ‘update’ operationissues: Array of validation issues with messages and pathsmessage: A formatted error message listing all issues
When schema validation occurs:
Schema validation happens only for client mutations - when you explicitly insert or update data:
- During inserts - When
collection.insert()is called - During updates - When
collection.update()is called
Schemas do not validate data coming from your server or sync layer. That data is assumed to already be valid.
const schema = z.object({ id: z.string(), created_at: z.string().transform(val => new Date(val)) // TInput: string, TOutput: Date})
// Validation happens here ✓collection.insert({ id: "1", created_at: "2024-01-01" // TInput: string})// If successful, stores: { created_at: Date } // TOutput: DateFor more details on schema validation and type transformations, see the Schemas guide.
Query Collection Error Tracking
Section titled “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:
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 statefunction 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, orundefinedif 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 bothlastErroranderrorCount:
Collection Status and Error States
Section titled “Collection Status and Error States”Collections track their status and transition between states:
import { useLiveQuery } from "@tanstack/react-db"
const TodoList = () => { const { data, status, isError, isLoading, isReady } = useLiveQuery( (query) => query.from({ todos: todoCollection }) )
if (isError) { return <div>Collection is in error state</div> }
if (isLoading) { return <div>Loading...</div> }
return <div>{data?.map(todo => <div key={todo.id}>{todo.text}</div>)}</div>}Collection status values:
idle- Not yet startedloading- Loading initial datainitialCommit- Processing initial dataready- Ready for useerror- In error statecleaned-up- Cleaned up and no longer usable
Using Suspense and Error Boundaries (React)
Section titled “Using Suspense and Error Boundaries (React)”For React applications, you can handle loading and error states with useLiveSuspenseQuery, React Suspense, and Error Boundaries:
import { useLiveSuspenseQuery } from "@tanstack/react-db"import { Suspense } from "react"import { ErrorBoundary } from "react-error-boundary"
const TodoList = () => { // No need to check status - Suspense and ErrorBoundary handle it const { data } = useLiveSuspenseQuery( (query) => query.from({ todos: todoCollection }) )
// data is always defined here return <div>{data.map(todo => <div key={todo.id}>{todo.text}</div>)}</div>}
const App = () => ( <ErrorBoundary fallback={<div>Failed to load todos</div>}> <Suspense fallback={<div>Loading...</div>}> <TodoList /> </Suspense> </ErrorBoundary>)With this approach, loading states are handled by <Suspense> and error states are handled by <ErrorBoundary> instead of within your component logic. See the React Suspense section in Live Queries for more details.
Transaction Error Handling
Section titled “Transaction Error Handling”When mutations fail, TanStack DB automatically rolls back optimistic updates:
const todoCollection = createCollection({ id: "todos", onInsert: async ({ transaction }) => { const response = await fetch("/api/todos", { method: "POST", body: JSON.stringify(transaction.mutations[0].modified), })
if (!response.ok) { // Throwing an error will rollback the optimistic state throw new Error(`HTTP Error: ${response.status}`) }
return response.json() },})
// Usage - optimistic update will be rolled back if the mutation failstry { const tx = todoCollection.insert({ id: "1", text: "New todo", completed: false, })
await tx.isPersisted.promise} catch (error) { // The optimistic update has been automatically rolled back console.error("Failed to create todo:", error)}Transaction States and Error Information
Section titled “Transaction States and Error Information”Transactions have the following states:
pending- Transaction is being processedpersisting- Currently executing the mutation functioncompleted- Transaction completed successfullyfailed- Transaction failed and was rolled back
Access transaction error information from collection operations:
const todoCollection = createCollection({ id: "todos", onUpdate: async ({ transaction }) => { const response = await fetch(`/api/todos/${transaction.mutations[0].key}`, { method: "PUT", body: JSON.stringify(transaction.mutations[0].modified), })
if (!response.ok) { throw new Error(`Update failed: ${response.status}`) } },})
try { const tx = await todoCollection.update("todo-1", (draft) => { draft.completed = true })
await tx.isPersisted.promise} catch (error) { // Transaction has been rolled back console.log(tx.state) // "failed" console.log(tx.error) // { message: "Update failed: 500", error: Error }}Or with manual transaction creation:
const tx = createTransaction({ mutationFn: async ({ transaction }) => { throw new Error("API failed") }})
tx.mutate(() => { collection.insert({ id: "1", text: "Item" })})
try { await tx.commit()} catch (error) { // Transaction has been rolled back console.log(tx.state) // "failed" console.log(tx.error) // { message: "API failed", error: Error }}Collection Operation Errors
Section titled “Collection Operation Errors”Invalid Collection State
Section titled “Invalid Collection State”Collections in an error state cannot perform operations and must be manually recovered:
import { CollectionInErrorStateError } from "@tanstack/db"
try { todoCollection.insert(newTodo)} catch (error) { if (error instanceof CollectionInErrorStateError) { // Collection needs to be cleaned up and restarted await todoCollection.cleanup()
// Now retry the operation todoCollection.insert(newTodo) }}Missing Mutation Handlers
Section titled “Missing Mutation Handlers”Direct mutations require handlers to be configured:
const todoCollection = createCollection({ id: "todos", getKey: (todo) => todo.id, // Missing onInsert handler})
// This will throw an errortodoCollection.insert(newTodo)// Error: Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configuredInsert Operation Errors
Section titled “Insert Operation Errors”DuplicateKeyError
Section titled “DuplicateKeyError”Thrown when inserting items with existing keys:
import { DuplicateKeyError } from "@tanstack/db"
try { todoCollection.insert({ id: "existing-id", text: "Todo" })} catch (error) { if (error instanceof DuplicateKeyError) { console.log(`Duplicate key: ${error.message}`) // Consider using update() instead, or check if item exists first }}UndefinedKeyError
Section titled “UndefinedKeyError”Thrown when an object is created without a defined key:
import { UndefinedKeyError } from "@tanstack/db"
const collection = createCollection({ id: "todos", getKey: (item) => item.id,})
try { collection.insert({ text: "Todo" }) // Missing 'id' field} catch (error) { if (error instanceof UndefinedKeyError) { console.log("Item is missing required key field") // Ensure your items have the key field defined by getKey }}Update Operation Errors
Section titled “Update Operation Errors”UpdateKeyNotFoundError
Section titled “UpdateKeyNotFoundError”Thrown when trying to update a key that doesn’t exist in the collection:
import { UpdateKeyNotFoundError } from "@tanstack/db"
try { todoCollection.update("nonexistent-key", draft => { draft.completed = true })} catch (error) { if (error instanceof UpdateKeyNotFoundError) { console.log("Key not found - item may have been deleted") // Consider using insert() if the item doesn't exist }}KeyUpdateNotAllowedError
Section titled “KeyUpdateNotAllowedError”Thrown when attempting to change an item’s key (not allowed - delete and re-insert instead):
import { KeyUpdateNotAllowedError } from "@tanstack/db"
try { todoCollection.update("todo-1", draft => { draft.id = "todo-2" // Not allowed! })} catch (error) { if (error instanceof KeyUpdateNotAllowedError) { console.log("Cannot change item keys") // Instead, delete the old item and insert a new one }}Delete Operation Errors
Section titled “Delete Operation Errors”DeleteKeyNotFoundError
Section titled “DeleteKeyNotFoundError”Thrown when trying to delete a key that doesn’t exist:
import { DeleteKeyNotFoundError } from "@tanstack/db"
try { todoCollection.delete("nonexistent-key")} catch (error) { if (error instanceof DeleteKeyNotFoundError) { console.log("Key not found - item may have already been deleted") // This may be acceptable in some scenarios (idempotent deletes) }}Sync Error Handling
Section titled “Sync Error Handling”Query Collection Sync Errors
Section titled “Query Collection Sync Errors”Query collections handle sync errors gracefully and mark the collection as ready even on error to avoid blocking applications:
import { queryCollectionOptions } from "@tanstack/query-db-collection"
const todoCollection = createCollection( queryCollectionOptions({ queryKey: ["todos"], queryFn: async () => { const response = await fetch("/api/todos") if (!response.ok) { throw new Error(`Failed to fetch: ${response.status}`) } return response.json() }, queryClient, getKey: (item) => item.id, schema: todoSchema, // Standard TanStack Query error handling options retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }))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
Section titled “Sync Write Errors”Sync functions must handle their own errors during write operations:
const collection = createCollection({ id: "todos", sync: { sync: ({ begin, write, commit }) => { begin()
try { // Will throw if key already exists write({ type: "insert", value: { id: "existing-id", text: "Todo" } }) } catch (error) { // Error: Cannot insert document with key "existing-id" from sync because it already exists }
commit() } }})Cleanup Error Handling
Section titled “Cleanup Error Handling”Cleanup errors are isolated to prevent blocking the cleanup process:
const collection = createCollection({ id: "todos", sync: { sync: ({ begin, commit }) => { begin() commit()
// Return a cleanup function return () => { // If this throws, the error is re-thrown in a microtask // but cleanup continues successfully throw new Error("Sync cleanup failed") } }, },})
// Cleanup completes even if the sync cleanup function throwsawait collection.cleanup() // Resolves successfully// Error is re-thrown asynchronously via queueMicrotaskError Recovery Patterns
Section titled “Error Recovery Patterns”Collection Cleanup and Restart
Section titled “Collection Cleanup and Restart”Clean up collections in error states:
if (todoCollection.status === "error") { // Cleanup will stop sync and reset the collection await todoCollection.cleanup()
// Collection will automatically restart on next access todoCollection.preload() // Or any other operation}Graceful Degradation
Section titled “Graceful Degradation”Collections continue to work with cached data even when sync fails:
const TodoApp = () => { const { data, isError } = useLiveQuery((query) => query.from({ todos: todoCollection }) )
return ( <div> {isError && ( <div>Sync failed, but you can still view cached data</div> )} {data?.map(todo => <TodoItem key={todo.id} todo={todo} />)} </div> )}Transaction Rollback Cascading
Section titled “Transaction Rollback Cascading”When a transaction fails, conflicting transactions are automatically rolled back:
const tx1 = createTransaction({ mutationFn: async () => {} })const tx2 = createTransaction({ mutationFn: async () => {} })
tx1.mutate(() => collection.update("1", draft => { draft.value = "A" }))tx2.mutate(() => collection.update("1", draft => { draft.value = "B" })) // Same item
// Rolling back tx1 will also rollback tx2 due to conflicttx1.rollback() // tx2 is automatically rolled backTransaction Lifecycle Errors
Section titled “Transaction Lifecycle Errors”Transactions validate their state before operations to prevent misuse. Here are the specific errors you may encounter:
MissingMutationFunctionError
Section titled “MissingMutationFunctionError”Thrown when creating a transaction without a required mutationFn:
import { MissingMutationFunctionError } from "@tanstack/db"
try { const tx = createTransaction({}) // Missing mutationFn} catch (error) { if (error instanceof MissingMutationFunctionError) { console.log("mutationFn is required when creating a transaction") }}TransactionNotPendingMutateError
Section titled “TransactionNotPendingMutateError”Thrown when calling mutate() after a transaction is no longer pending:
import { TransactionNotPendingMutateError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })
await tx.commit()
try { tx.mutate(() => { collection.insert({ id: "1", text: "Item" }) })} catch (error) { if (error instanceof TransactionNotPendingMutateError) { console.log("Cannot mutate - transaction is no longer pending") }}TransactionNotPendingCommitError
Section titled “TransactionNotPendingCommitError”Thrown when calling commit() after a transaction is no longer pending:
import { TransactionNotPendingCommitError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })tx.mutate(() => collection.insert({ id: "1", text: "Item" }))
await tx.commit()
try { await tx.commit() // Trying to commit again} catch (error) { if (error instanceof TransactionNotPendingCommitError) { console.log("Transaction already committed") }}TransactionAlreadyCompletedRollbackError
Section titled “TransactionAlreadyCompletedRollbackError”Thrown when calling rollback() on a transaction that’s already completed:
import { TransactionAlreadyCompletedRollbackError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })tx.mutate(() => collection.insert({ id: "1", text: "Item" }))
await tx.commit()
try { tx.rollback() // Can't rollback after commit} catch (error) { if (error instanceof TransactionAlreadyCompletedRollbackError) { console.log("Cannot rollback - transaction already completed") }}Sync Transaction Errors
Section titled “Sync Transaction Errors”When working with sync transactions, these errors can occur:
NoPendingSyncTransactionWriteError
Section titled “NoPendingSyncTransactionWriteError”Thrown when calling write() without an active sync transaction:
const collection = createCollection({ id: "todos", sync: { sync: ({ write }) => { // Calling write without begin() first write({ type: "insert", value: { id: "1", text: "Todo" } }) // Error: No pending sync transaction to write to } }})SyncTransactionAlreadyCommittedWriteError
Section titled “SyncTransactionAlreadyCommittedWriteError”Thrown when calling write() after the sync transaction is already committed:
const collection = createCollection({ id: "todos", sync: { sync: ({ begin, write, commit }) => { begin() commit()
// Trying to write after commit write({ type: "insert", value: { id: "1", text: "Todo" } }) // Error: The pending sync transaction is already committed } }})NoPendingSyncTransactionCommitError
Section titled “NoPendingSyncTransactionCommitError”Thrown when calling commit() without an active sync transaction.
SyncTransactionAlreadyCommittedError
Section titled “SyncTransactionAlreadyCommittedError”Thrown when calling commit() on a sync transaction that’s already committed.
Best Practices
Section titled “Best Practices”-
Use instanceof checks - Use
instanceofinstead of string matching for error handling:// ✅ Good - type-safe error handlingif (error instanceof SchemaValidationError) {// Handle validation error}// ❌ Avoid - brittle string matchingif (error.message.includes("validation failed")) {// Handle validation error} -
Import specific error types - Import only the error classes you need for better tree-shaking
-
Always handle SchemaValidationError - Provide clear feedback for validation failures
-
Check collection status - Use
isError,isLoading,isReadyflags in React components -
Handle transaction promises - Always handle
isPersisted.promiserejections
Example: Complete Error Handling
Section titled “Example: Complete Error Handling”import { createCollection, SchemaValidationError, DuplicateKeyError, UpdateKeyNotFoundError, DeleteKeyNotFoundError, TransactionNotPendingCommitError, createTransaction} from "@tanstack/db"import { useLiveQuery } from "@tanstack/react-db"
const todoCollection = createCollection({ id: "todos", schema: todoSchema, getKey: (todo) => todo.id, onInsert: async ({ transaction }) => { const response = await fetch("/api/todos", { method: "POST", body: JSON.stringify(transaction.mutations[0].modified), })
if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) }
return response.json() }, sync: { sync: ({ begin, write, commit }) => { // Your sync implementation begin() // ... sync logic commit() } }})
const TodoApp = () => { const { data, status, isError, isLoading } = useLiveQuery( (query) => query.from({ todos: todoCollection }) )
const handleAddTodo = async (text: string) => { try { const tx = await todoCollection.insert({ id: crypto.randomUUID(), text, completed: false, })
// Wait for persistence await tx.isPersisted.promise } catch (error) { if (error instanceof SchemaValidationError) { alert(`Validation error: ${error.issues[0]?.message}`) } else if (error instanceof DuplicateKeyError) { alert("A todo with this ID already exists") } else { alert(`Failed to add todo: ${error.message}`) } } }
const handleCleanup = async () => { try { await todoCollection.cleanup() // Collection will restart on next access } catch (error) { console.error("Cleanup failed:", error) } }
if (isError) { return ( <div> <div>Collection error - data may be stale</div> <button onClick={handleCleanup}> Restart Collection </button> </div> ) }
if (isLoading) { return <div>Loading todos...</div> }
return ( <div> <button onClick={() => handleAddTodo("New todo")}> Add Todo </button> {data?.map(todo => ( <div key={todo.id}>{todo.text}</div> ))} </div> )}See Also
Section titled “See Also”- API Reference - Detailed API documentation
- Mutations Guide - Learn about optimistic updates and rollbacks
- TanStack Query Error Handling - Query-specific error handling