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
- Read error.errors ([rollback-to-savepoint error, release error]) with cause = the callback error; the cleanup failures usually point at a dead connection.
- Ensure close()/connection teardown can't race in-flight transactions (await outstanding work first).
- 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
- Await all transactions before closing the database.
- Avoid deep savepoint nesting in long-running operations.
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
- Database transaction and rollback both failed
- Nested transactions require database savepoint support
- Cannot call ' + method + '() from a transaction-scoped datab
- Unknown transaction token: + token.id
- MySQL migration lock is already held by this database
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/9c26c4614bb6b581.
Report an issue: GitHub.