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

  1. Enable WAL mode and busy_timeout (PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000) so writers don't deadlock
  2. Ensure a single write connection/process for the database file; queue writes through one driver
  3. 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

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


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