remix-run/remix · critical · AggregateError
SQLite rollback and connection cleanup both failed
Error message
SQLite rollback and connection cleanup both failed
What it means
When ROLLBACK fails in rollbackTransaction, the driver tries to discard the uncertain connection as recovery; if that cleanup also fails it throws an AggregateError of both failures. The connection state is unknown, so the driver surfaces everything rather than masking the recovery error.
Source
Thrown at packages/data-table-sqlite/src/lib/driver.ts:377
} finally {
this.#transactions.delete(token.id)
}
}
/**
* Rolls back an open sqlite transaction.
* @param token Transaction token to roll back.
* @returns A promise that resolves when the transaction is rolled back.
*/
async rollbackTransaction(token: TransactionToken): Promise<void> {
this.#assertTransaction(token)
try {
this.#database.exec('rollback')
} catch (rollbackError) {
try {
this.#discardUncertainConnection()
} catch (recoveryError) {
throw new AggregateError(
[rollbackError, recoveryError],
'SQLite rollback and connection cleanup both failed',
{ cause: rollbackError },
)
}
throw rollbackError
} finally {
this.#transactions.delete(token.id)
}
}
/**
* Creates a savepoint in an open sqlite transaction.
* @param token Transaction token to use.
* @param name Savepoint name.
* @returns A promise that resolves when the savepoint is created.
*/
async createSavepoint(token: TransactionToken, name: string): Promise<void> {View on GitHub (pinned to 9696913134)
Solutions
- Read AggregateError.errors — fix the primary rollback cause first (locks: WAL + busy_timeout + single writer; I/O: check disk space and mount)
- Restart/reset the connection or process after this error; do not reuse the driver instance
- Run PRAGMA integrity_check after recovery to verify the database file
Example fix
// single-writer + WAL avoids most busy rollbacks
await db.exec('pragma journal_mode = wal')
await db.exec('pragma busy_timeout = 5000') Defensive patterns
Strategy: try-catch
Validate before calling
await db.exec('pragma busy_timeout = 5000') // reduce busy-driven rollback failures Try / catch
try { await db.rollbackTransaction(tx) } catch (e) { if (e instanceof AggregateError) { /* connection uncertain: recreate driver, run integrity_check */ } throw e } Prevention
- Serialize writes through one connection
- Use WAL mode
- After this error, recreate the connection and verify integrity
When it happens
Trigger: ROLLBACK erroring due to a lock (SQLITE_BUSY), I/O error, or corrupted database, and the subsequent connection close/discard also throwing — e.g. native client in a broken state or file unmountable.
Common situations: Concurrent writers holding locks past busy_timeout; disk/NFS failures mid-transaction; native module (better-sqlite3/node:sqlite) crashing during close after a failed statement.
Related errors
- SQLite commit and rollback both failed
- SQLite database cannot ' + method + ' while transactions are
- Unknown transaction token: ' + token.id
- Database transaction and rollback both failed
- expected error to be ${describeExpectedError(args[0])}, got
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/87f6dd2d00f96948.
Report an issue: GitHub.