remix-run/remix · critical · Error
SQLite database is closed
Error message
SQLite database is closed
What it means
Thrown by the SQLite data-table driver when an operation is attempted after the underlying SQLite database has been closed. The driver tracks an internal #databaseOpen flag; #assertDatabaseOpen() runs before execute, executeScript, hasTable, hasColumn, and transaction methods and throws immediately if the flag is false. It protects against use-after-close on a driver instance.
Source
Thrown at packages/data-table-sqlite/src/lib/driver.ts:470
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
const REMOVE_RETRY_DELAY_MS = 100
async function removeDatabaseFile(filename: string): Promise<void> {
// Windows keeps a just-closed database file locked for a short window (deferred handle
// release, antivirus scans), so removal is retried with a linear backoff
for (let attempt = 0; ; attempt++) {
try {
await rm(filename, { force: true })
return
} catch (error) {
if (attempt >= REMOVE_RETRIES || !isRetryableRemoveError(error)) {
throw error
}View on GitHub (pinned to 9696913134)
Solutions
- Ensure close() is only called after all in-flight queries, migrations, and transactions have settled (track outstanding promises before closing).
- If you need the database again after closing, create and open a fresh driver/database instance instead of reusing the closed one.
- In tests, use beforeEach/afterEach pairs so each test opens and closes its own driver and no shared instance outlives close.
Example fix
// before
await db.close()
await db.hasTable('users') // throws
// after
await db.close()
db = new SqliteDriver(...) // reopen a fresh instance
await db.hasTable('users') Defensive patterns
Strategy: try-catch
Validate before calling
// Track closes in your own wrapper since the flag is private
let closed = false
export async function withDb<T>(fn: () => Promise<T>): Promise<T> {
if (closed) throw new Error('db already closed')
return await fn()
} Try / catch
try {
await db.hasTable('users')
} catch (error) {
if (error instanceof Error && error.message === 'SQLite database is closed') {
// reopen a new driver instance or bail gracefully
}
} Prevention
- Track close() in app lifecycle code and gate new queries on it.
- Await all in-flight queries before closing in shutdown handlers.
- In tests, open/close the driver per test rather than sharing one instance.
When it happens
Trigger: Calling db.execute(...), db.executeScript(...), db.hasTable(...), db.hasColumn(...), beginTransaction(), or any statement inside a transaction after close() (or a failed open/remove cycle) has already run on the same SQLite driver instance.
Common situations: App shutdown handlers that close the DB then a pending request or migration still tries to query; reusing a module-level driver singleton after tests call afterAll/close; hot-reload creating a new database file lifecycle while old handles retry writes.
Related errors
- MySQL database cannot + method + while transactions are op
- SQLite database cannot ' + method + ' while transactions are
- Unsupported operation kind
- Cannot call ' + method + '() from a transaction-scoped datab
- expected error to be ${describeExpectedError(args[0])}, got
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/b5bc5e2872edfc6f.
Report an issue: GitHub.