remix-run/remix · error · Error

SQLite database cannot ' + method + ' while transactions are

Error message

SQLite database cannot ' + method + ' while transactions are open

What it means

The SQLite driver blocks lifecycle operations (wipe, close) while transactions remain open, throwing from #assertNoOpenTransactions when the transactions map is non-empty — same protection as the Postgres driver, preventing resets that would corrupt in-flight work.

Source

Thrown at packages/data-table-sqlite/src/lib/driver.ts:457

      this.#replaceDatabase()
    } else {
      // The supplied client remains caller-owned, but this wrapper cannot safely
      // reuse a connection whose transaction state is unknown.
      this.#databaseOpen = false
    }
  }

  #configOrThrow(method: string): SqliteDatabaseConfig {
    if (!this.#config) {
      throw new Error('SQLite database ' + method + '() requires config-based construction')
    }

    return this.#config
  }

  #assertNoOpenTransactions(method: string): void {
    if (this.#transactions.size > 0) {
      throw new Error('SQLite database cannot ' + method + ' while transactions are open')
    }
  }

  #assertTransaction(token: TransactionToken): void {
    this.#assertDatabaseOpen()
    if (!this.#transactions.has(token.id)) {
      throw new Error('Unknown transaction token: ' + token.id)
    }
  }

  #assertDatabaseOpen(): void {
    if (!this.#databaseOpen) {
      throw new Error('SQLite database is closed')
    }
  }
}

const REMOVE_RETRIES = 10

View on GitHub (pinned to 9696913134)

Solutions

  1. Guarantee commit/rollback in try/finally around every transaction, or use the managed transaction API
  2. Await all in-flight work before wipe()/close()
  3. In tests, use per-test drivers or ensure afterAll ordering runs after transactions settle

Example fix

// before
let tx = await db.beginTransaction()
// leaked on early return
await db.wipe() // throws

// after
let tx = await db.beginTransaction()
try { await work(tx); await db.commitTransaction(tx) }
catch (e) { await db.rollbackTransaction(tx); throw e }
Defensive patterns

Strategy: validation

Validate before calling

// drain before teardown
await Promise.allSettled(pendingTransactionWork)
await db.wipe()

Try / catch

try { await db.close() } catch (e) { if (e instanceof Error && e.message.includes('while transactions are open')) { /* settle or force-rollback open transactions, then retry */ } throw e }

Prevention

When it happens

Trigger: Calling wipe() or close() after a beginTransaction without a matching commit/rollback; leaked tokens from early returns; a still-running async operation holding a transaction during test teardown.

Common situations: Test teardown wiping SQLite while queries are pending; error paths that begin but never roll back; close() in a process shutdown hook racing in-flight transactions.

Related errors


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