remix-run/remix · critical · AggregateError

Database transaction and rollback both failed

Error message

Database transaction and rollback both failed

What it means

Inside transaction(), if the callback throws, the driver's rollbackTransaction is attempted. If the rollback itself also throws, both errors are bundled into an AggregateError so the original failure (also set as cause) is not lost. This error means the connection/transaction is in a broken state (often the connection died mid-transaction).

Source

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

    if (!this.#token) {
      let token = await this.#driver.beginTransaction(options)
      let tx = Database.#createInternalDatabase(
        this.#driver,
        { now: this.#now },
        {
          token,
          savepointCounter: this.#savepointCounter,
        },
      )

      let result: result
      try {
        result = await callback(tx)
      } 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

View on GitHub (pinned to 9696913134)

Solutions

  1. Inspect error.errors[0] (the callback error) and error.errors[1] (the rollback error) to find the root cause; the rollback error usually reveals the connection problem.
  2. Ensure the database file/connection is available and not exclusively locked; verify close/remove doesn't race transactions.
  3. Make the operation idempotent and retry the whole transaction on a fresh connection if the DB was temporarily unavailable.

Example fix

// before
try { await db.transaction(cb) } catch (e) { console.log(e.message) }

// after
try {
  await db.transaction(cb)
} catch (error) {
  for (let inner of error.errors ?? [error]) console.error(inner)
}
Defensive patterns

Strategy: retry

Validate before calling

// Probe the connection before starting a transaction
await db.hasTable('some_table') // throws early if closed/unavailable
await db.transaction(cb)

Try / catch

try {
  await db.transaction(cb)
} catch (error) {
  if (error instanceof AggregateError) {
    const [callbackError, rollbackError] = error.errors
    // log both; retry once if rollbackError indicates a dead connection
  }
}

Prevention

When it happens

Trigger: A transaction callback throws AND the ROLLBACK fails — connection dropped (SQLite file deleted, network DB disconnected), the transaction was already auto-rolled-back by the server, or the driver handle is closed.

Common situations: Database file removed or locked during a transaction; connection timeouts; nested error handling where the first error killed the session.

Related errors


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