remix-run/remix · critical · AggregateError
SQLite commit and rollback both failed
Error message
SQLite commit and rollback both failed
What it means
During commitTransaction the SQLite driver attempts COMMIT; if that fails it tries ROLLBACK as recovery, and if both fail it discards the uncertain connection and throws an AggregateError containing all failures. It signals the connection's state is unknowable and the database file/connection was reset.
Source
Thrown at packages/data-table-sqlite/src/lib/driver.ts:354
* Commits an open sqlite transaction.
* @param token Transaction token to commit.
* @returns A promise that resolves when the transaction is committed.
*/
async commitTransaction(token: TransactionToken): Promise<void> {
this.#assertTransaction(token)
try {
this.#database.exec('commit')
} catch (commitError) {
try {
this.#database.exec('rollback')
} catch (rollbackError) {
let failures: unknown[] = [commitError, rollbackError]
try {
this.#discardUncertainConnection()
} catch (recoveryError) {
failures.push(recoveryError)
}
throw new AggregateError(failures, 'SQLite commit and rollback both failed', {
cause: commitError,
})
}
throw commitError
} 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')View on GitHub (pinned to 9696913134)
Solutions
- Enable WAL mode and busy_timeout (PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000) so writers don't deadlock
- Ensure a single write connection/process for the database file; queue writes through one driver
- Inspect errors[0..n] of the AggregateError for the root cause (disk, lock, corruption) and address it; if the file may be corrupt, restore from backup or run integrity_check
Example fix
// after opening the database
await db.exec('pragma journal_mode = wal')
await db.exec('pragma busy_timeout = 5000') Defensive patterns
Strategy: try-catch
Validate before calling
// prevent the common causes up front
await db.exec('pragma journal_mode = wal')
await db.exec('pragma busy_timeout = 5000') Try / catch
try { await db.commitTransaction(tx) } catch (e) { if (e instanceof AggregateError) { for (const inner of e.errors) console.error(inner); /* rebuild connection, do not reuse driver */ } throw e } Prevention
- Enable WAL and busy_timeout
- Single writer connection per database file
- Monitor disk space; treat AggregateError as fatal for the connection
When it happens
Trigger: COMMIT failing due to a deferred foreign-key violation, disk I/O error, or database lock (another connection holding an exclusive lock), followed by ROLLBACK also failing (e.g. lock persists, disk full, corrupted file).
Common situations: Multiple processes/connections writing the same SQLite file concurrently; disk-full or NFS-mounted database files; heavy concurrent load without WAL mode causing SQLITE_BUSY on commit.
Related errors
- SQLite rollback and connection cleanup both failed
- SQLite database cannot ' + method + ' while transactions are
- Unknown transaction token: ' + token.id
- expected error to be ${describeExpectedError(args[0])}, got
- Unknown transaction token: + token.id
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/a5ad2aa6f5c4f0f4.
Report an issue: GitHub.