remix-run/remix · error · DataTableQueryError

Nested transactions require database savepoint support

Error message

Nested transactions require database savepoint support

What it means

Calling transaction() while already inside a transaction creates a nested transaction, implemented with SAVEPOINTs. The Database checks capabilities.savepoints first and throws if the driver doesn't support them, because a fake nested transaction would risk committing partial inner work.

Source

Thrown at packages/data-table/src/lib/database.ts:930

      } catch (error) {
        try {
          await this.#driver.rollbackTransaction(token)
        } catch (rollbackError) {
          throw new AggregateError(
            [error, rollbackError],
            'Database transaction and rollback both failed',
            { cause: error },
          )
        }
        throw error
      }

      await this.#driver.commitTransaction(token)
      return result
    }

    if (!this.capabilities.savepoints) {
      throw new DataTableQueryError('Nested transactions require database savepoint support')
    }

    let savepointName = 'sp_' + String(this.#savepointCounter.value)
    this.#savepointCounter.value += 1

    await this.#driver.createSavepoint(this.#token, savepointName)

    let result: result
    try {
      result = await callback(this)
    } catch (error) {
      let failures: unknown[] = [error]
      try {
        await this.#driver.rollbackToSavepoint(this.#token, savepointName)
      } catch (rollbackError) {
        failures.push(rollbackError)
      }
      try {

View on GitHub (pinned to 9696913134)

Solutions

  1. Restructure code so transactions aren't nested: accept the outer tx as a parameter instead of opening a new one (the classic 'transaction-in-transaction' refactor).
  2. Driver authors: implement savepoints and set capabilities.savepoints = true.
  3. Branch on db.capabilities.savepoints to decide whether nesting is allowed on the current driver.

Example fix

// before
async function saveThing(db, data) {
  await db.transaction(async (tx) => { ... })
}
await db.transaction(async (tx) => saveThing(tx, data))

// after
async function saveThing(db, data) {
  await db.transaction(async (tx) => { ... })
}
// call saveThing(db, data) at top level, not inside another transaction
Defensive patterns

Strategy: validation

Validate before calling

if (depth > 0 && !db.capabilities.savepoints) {
  return callback(db) // run without a nested transaction
}
return db.transaction(callback)

Prevention

When it happens

Trigger: db.transaction(async (tx) => { await tx.transaction(cb) }) on a driver whose capabilities.savepoints is false (e.g. a minimal custom driver or a dialect without savepoint support).

Common situations: Custom drivers that never implemented createSavepoint/releaseSavepoint; helper functions that open their own transaction being called from inside another transaction.

Related errors


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