remix-run/remix · error · DataTableQueryError

Cannot call ' + method + '() from a transaction-scoped…

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.

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.

Appendix: 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)