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

  1. Call lifecycle methods only on the root database, outside any transaction callback.
  2. 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

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


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/30755897389c3cdf. Report an issue: GitHub.