remix-run/remix · critical · AggregateError

Nested transaction cleanup failed

Error message

Nested transaction cleanup failed

What it means

When a nested (savepoint) transaction callback throws, the Database rolls back to the savepoint and releases it. If both the rollback-to-savepoint and the releaseSavepoint fail, the failures are collected and thrown as an AggregateError with the original error as cause — the savepoint machinery itself is broken, typically because the connection died.

Source

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

    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 {
        await this.#driver.releaseSavepoint(this.#token, savepointName)
      } catch (releaseError) {
        failures.push(releaseError)
      }
      if (failures.length > 1) {
        throw new AggregateError(failures, 'Nested transaction cleanup failed', { cause: error })
      }
      throw error
    }

    await this.#driver.releaseSavepoint(this.#token, savepointName)
    return result
  }

  async #executeOperation(operation: DataManipulationOperation): Promise<DataManipulationResult> {
    try {
      return await this.#driver.execute({
        operation,
        transaction: this.#token,
      })
    } catch (error) {
      throw new DataTableDatabaseError('Database execution failed', {
        cause: error,
        metadata: {

View on GitHub (pinned to 9696913134)

Solutions

  1. Read error.errors ([rollback-to-savepoint error, release error]) with cause = the callback error; the cleanup failures usually point at a dead connection.
  2. Ensure close()/connection teardown can't race in-flight transactions (await outstanding work first).
  3. Retry the outer transaction on a fresh connection once the database is available again.
Defensive patterns

Strategy: retry

Validate before calling

// Ensure the connection is alive before nested transactions
await db.hasTable('some_table')
await db.transaction(async (tx) => tx.transaction(inner))

Try / catch

try {
  await db.transaction(outer)
} catch (error) {
  if (error instanceof AggregateError && /Nested transaction cleanup/.test(error.message)) {
    // connection-level failure; inspect error.errors and retry on a fresh db
  }
}

Prevention

When it happens

Trigger: A nested transaction callback throws while the underlying connection is broken (database closed, file removed, network dropped), so both savepoint cleanup calls fail.

Common situations: Closing the DB or dropping the connection while nested transactions are in flight; SQLite file lock/remove during tests; long transactions hitting driver timeouts.

Related errors


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