remix-run/remix · error · DataTableQueryError
Cannot call ' + method + '() from a transaction-scoped datab
Error message
Cannot call ' + method + '() from a transaction-scoped database
What it means
When you call db.transaction(cb), the callback receives a transaction-scoped Database whose lifecycle is owned by the outer transaction. Lifecycle methods (close, wipe, migrate, migrationStatus, reset) are blocked on that inner handle via #assertLifecycleOperationAllowed because committing/rolling back or closing the connection is the outer transaction's job.
Source
Thrown at packages/data-table/src/lib/database.ts:514
/**
* Wipes the database, applies migrations, and optionally seeds data.
*
* @param options Migrations and optional seed function used to rebuild the database.
* @returns A promise that resolves when the database has been rebuilt.
*/
async reset(options: DatabaseResetOptions): Promise<void> {
this.#assertLifecycleOperationAllowed('reset')
await this.wipe()
await this.migrate(options.migrations, { journalTable: options.journalTable })
await options.seed?.(this)
}
#assertLifecycleOperationAllowed(
method: 'close' | 'migrate' | 'migrationStatus' | 'reset' | 'wipe',
): void {
if (this.#token) {
throw new DataTableQueryError(
'Cannot call ' + method + '() from a transaction-scoped database',
)
}
}
now(): unknown {
return this.#now()
}
query<
tableName extends string,
row extends Record<string, unknown>,
primaryKey extends readonly (keyof row & string)[],
>(
table: QueryTableInput<tableName, row, primaryKey>,
): QueryObject<
QueryTableInput<tableName, row, primaryKey>,
Pretty<QueryColumnTypeMapFromRow<tableName, row>>,View on GitHub (pinned to 9696913134)
Solutions
- Call lifecycle methods only on the root database, outside any transaction callback.
- Refactor helpers so they don't manage schema/close, or pass the root db explicitly to schema-management code.
Example fix
// before
await db.transaction(async (tx) => {
await tx.migrate(migrations)
})
// after
await db.migrate(migrations)
await db.transaction(async (tx) => { ... }) Defensive patterns
Strategy: try-catch
Validate before calling
// Discipline: never pass the tx handle to schema-management code.
// Approximate guard by tracking which handle you passed around
function assertRootDatabase(db: Database) {
if (Object.is(db, currentTransactionHandle)) {
throw new Error('Pass the root database, not the tx')
}
} Type guard
function isTransactionScoped(db: Database): boolean {
return Object.is(db, currentTransactionHandle)
} Try / catch
try {
await tx.migrate(migrations)
} catch (error) {
if (error instanceof DataTableQueryError && /transaction-scoped/.test(error.message)) {
// move the call outside the transaction
}
} Prevention
- Keep migrate/reset/wipe/close calls at app startup, never inside transaction callbacks.
- Give helpers an explicit db parameter that only ever receives the root Database.
When it happens
Trigger: Calling tx.close(), tx.migrate(...), tx.migrationStatus(), tx.reset(), or tx.wipe() on the database passed into a transaction callback.
Common situations: Passing the tx-scoped db deep into helpers (e.g. a wipe/migrate utility or test setup function) that normally receive the root db; refactors where a helper that manages schema is now called inside transactional test wrappers.
Related errors
- MySQL database cannot + method + while transactions are op
- SQLite database is closed
- Database transaction and rollback both failed
- Nested transactions require database savepoint support
- Nested transaction cleanup failed
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/30755897389c3cdf.
Report an issue: GitHub.