错误处理

错误处理

TanStack DB 提供全面的错误处理功能,以确保数据同步和状态管理的健壮性。本指南涵盖内置的错误处理机制以及如何有效地使用它们。

错误类型

TanStack DB 提供命名错误类,以实现更好的错误处理和类型安全。所有错误类都可以从 @tanstack/db (或更常见的是,特定于框架的包,例如 @tanstack/react-db) 导入。

ts
import {
  SchemaValidationError,
  CollectionInErrorStateError,
  DuplicateKeyError,
  MissingHandlerError,
  TransactionError,
  // ... and many more
} from "@tanstack/db"
import {
  SchemaValidationError,
  CollectionInErrorStateError,
  DuplicateKeyError,
  MissingHandlerError,
  TransactionError,
  // ... and many more
} from "@tanstack/db"

SchemaValidationError

在插入或更新操作期间,当数据与集合的模式不匹配时会抛出此错误。

ts
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"] }
  }
}
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"] }
  }
}

错误包含:

  • type:是“insert”还是“update”操作。
  • issues:包含消息和路径的验证问题数组。
  • message:列出所有问题的格式化错误消息。

集合状态和错误状态

集合跟踪其状态并在状态之间转换。

tsx
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>
}
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>
}

集合状态值:

  • idle - 尚未开始。
  • loading - 加载初始数据。
  • initialCommit - 处理初始数据。
  • ready - 已准备好使用。
  • error - 处于错误状态。
  • cleaned-up - 已清理,不再可用。

事务错误处理

当修改失败时,TanStack DB 会自动回滚乐观更新。

ts
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 fails
try {
  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)
}
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 fails
try {
  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)
}

事务状态和错误信息

事务具有以下状态:

  • pending - 事务正在处理中。
  • persisting - 当前正在执行修改函数。
  • completed - 事务已成功完成。
  • failed - 事务失败并已回滚。

从集合操作中访问事务错误信息。

ts
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 }
}
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 }
}

或通过手动创建事务。

ts
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 }
}
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 }
}

集合操作错误

无效的集合状态

处于 error 状态的集合无法执行操作,必须手动恢复。

ts
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)
  }
}
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)
  }
}

缺失的修改处理器

直接修改需要配置处理器。

ts
const todoCollection = createCollection({
  id: "todos",
  getKey: (todo) => todo.id,
  // Missing onInsert handler
})

// This will throw an error
todoCollection.insert(newTodo)
// Error: Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured
const todoCollection = createCollection({
  id: "todos",
  getKey: (todo) => todo.id,
  // Missing onInsert handler
})

// This will throw an error
todoCollection.insert(newTodo)
// Error: Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured

重复键错误

插入具有现有键的项目将抛出错误。

ts
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}`)
  }
}
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}`)
  }
}

模式验证错误

模式验证必须是同步的。

ts
const todoCollection = createCollection({
  id: "todos",
  getKey: (todo) => todo.id,
  schema: {
    "~standard": {
      validate: async (data) => { // Async validation not allowed
        // ...
      }
    }
  }
})

// Will throw: Schema validation must be synchronous
const todoCollection = createCollection({
  id: "todos",
  getKey: (todo) => todo.id,
  schema: {
    "~standard": {
      validate: async (data) => { // Async validation not allowed
        // ...
      }
    }
  }
})

// Will throw: Schema validation must be synchronous

同步错误处理

查询集合同步错误

查询集合会优雅地处理同步错误,即使出现错误也会将集合标记为就绪,以避免阻塞应用程序。

ts
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),
  })
)
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),
  })
)

发生同步错误时:

  • 错误被记录到控制台:[QueryCollection] Error observing query...
  • 集合被标记为就绪,以防止阻塞应用程序。
  • 缓存数据仍然可用。

同步写入错误

同步函数必须处理其自身的写入操作期间的错误。

ts
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()
    }
  }
})
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()
    }
  }
})

清理错误处理

清理错误是隔离的,以防止阻塞清理过程。

ts
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 throws
await collection.cleanup() // Resolves successfully
// Error is re-thrown asynchronously via queueMicrotask
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 throws
await collection.cleanup() // Resolves successfully
// Error is re-thrown asynchronously via queueMicrotask

错误恢复模式

集合清理和重启

清理处于错误状态的集合。

ts
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
}
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
}

优雅降级

即使同步失败,集合仍可使用缓存数据继续运行。

tsx
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>
  )
}
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>
  )
}

事务回滚级联

当事务失败时,冲突的事务会自动回滚。

ts
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 conflict
tx1.rollback() // tx2 is 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 conflict
tx1.rollback() // tx2 is automatically rolled back

处理无效状态错误

事务在操作前验证其状态。

ts
const tx = createTransaction({ mutationFn: async () => {} })

// Complete the transaction
await tx.commit()

// These will throw:
tx.mutate(() => {}) // Error: You can no longer call .mutate() as the transaction is no longer pending
tx.commit() // Error: You can no longer call .commit() as the transaction is no longer pending
tx.rollback() // Error: You can no longer call .rollback() as the transaction is already completed
const tx = createTransaction({ mutationFn: async () => {} })

// Complete the transaction
await tx.commit()

// These will throw:
tx.mutate(() => {}) // Error: You can no longer call .mutate() as the transaction is no longer pending
tx.commit() // Error: You can no longer call .commit() as the transaction is no longer pending
tx.rollback() // Error: You can no longer call .rollback() as the transaction is already completed

最佳实践

  1. 使用 instanceof 检查 - 使用 instanceof 而不是字符串匹配来进行错误处理。

    ts
    // ✅ Good - type-safe error handling
    if (error instanceof SchemaValidationError) {
      // Handle validation error
    }
    
    // ❌ Avoid - brittle string matching  
    if (error.message.includes("validation failed")) {
      // Handle validation error
    }
    
    // ✅ Good - type-safe error handling
    if (error instanceof SchemaValidationError) {
      // Handle validation error
    }
    
    // ❌ Avoid - brittle string matching  
    if (error.message.includes("validation failed")) {
      // Handle validation error
    }
    
  2. 导入特定的错误类型 - 仅导入您需要的错误类,以实现更好的 tree-shaking。

  3. 始终处理 SchemaValidationError - 为验证失败提供清晰的反馈。

  4. 检查集合状态 - 在 React 组件中使用 isErrorisLoadingisReady 标志。

  5. 处理事务 Promise - 始终处理 isPersisted.promise 的拒绝。

示例:完整的错误处理

tsx
import { 
  createCollection, 
  SchemaValidationError,
  DuplicateKeyError,
  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>
  )
}
import { 
  createCollection, 
  SchemaValidationError,
  DuplicateKeyError,
  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>
  )
}

另请参阅

我们的合作伙伴
Code Rabbit
Electric
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。