This error occurs when better-sqlite3 tries to execute a query while the database connection is already processing another query. It commonly happens when attempting to run an UPDATE or DELETE while iterating through a SELECT result set, or when trying to close the database while a query is still running.
In better-sqlite3, each database connection can only process one query at a time. Unlike some database drivers that handle concurrent operations automatically, better-sqlite3 is a synchronous SQLite wrapper that maintains strict sequential execution. The error "This database connection is busy executing a query" occurs when: 1. **Attempting nested queries** while iterating through results with `.iterate()` 2. **Calling methods** on the database while a statement is still executing 3. **Closing the connection** while an active iterator or query is still in progress 4. **Multiple statement execution** on the same connection without completing the previous operation This is fundamentally different from SQLITE_BUSY (database file locked). This error is a programming issue - you're trying to do multiple things with one connection simultaneously.
better-sqlite3 is a synchronous library that executes queries one at a time per connection. Unlike async database drivers:
- Each connection has a single execution queue
- Operations block until completion
- No concurrent queries on one connection
- Iterators must be fully consumed before running other queries
Good pattern:
const Database = require('better-sqlite3');
const db = new Database('database.db');
// Fetch all results first
const users = db.prepare('SELECT * FROM users').all();
// Then iterate and modify
for (const user of users) {
db.prepare('UPDATE users SET active = 1 WHERE id = ?').run(user.id);
}Bad pattern:
// Don't try to iterate and modify simultaneously
for (const row of db.prepare('SELECT * FROM users').iterate()) {
// ERROR: connection is busy with the SELECT iterator
db.prepare('UPDATE users SET status = ? WHERE id = ?')
.run('active', row.id);
}The simplest solution is to use .all() instead of .iterate(). This fetches all results into memory at once:
const Database = require('better-sqlite3');
const db = new Database('database.db');
// Use .all() instead of .iterate()
const rows = db.prepare('SELECT id, name FROM large_table').all();
// Now you can safely run other queries
for (const row of rows) {
db.prepare('UPDATE large_table SET processed = 1 WHERE id = ?').run(row.id);
}
db.close();When to use each method:
- .all(): All results fit in memory, want simplicity
- .get(): Single row, immediate operation
- .iterate(): Memory-constrained, processing doesn't need the connection
- .raw(): Performance-critical, need raw arrays
Example with all() and transactions:
const db = new Database('database.db');
const allUsers = db.prepare('SELECT id FROM users WHERE inactive = 1').all();
// Wrap updates in a transaction
const updateUser = db.prepare('UPDATE users SET active = 1 WHERE id = ?');
const transaction = db.transaction((ids) => {
for (const id of ids) {
updateUser.run(id);
}
});
transaction(allUsers.map(u => u.id));
db.close();When modifying multiple rows, wrap operations in a transaction. This is both faster and cleaner:
const Database = require('better-sqlite3');
const db = new Database('database.db');
// Define your prepared statements
const getRows = db.prepare('SELECT id, value FROM data WHERE processed = 0');
const updateRow = db.prepare('UPDATE data SET processed = 1 WHERE id = ?');
// Create a transaction function
const processRows = db.transaction(() => {
const rows = getRows.all();
for (const row of rows) {
updateRow.run(row.id);
}
return rows.length;
});
// Execute the transaction
const count = processRows();
console.log(`Processed ${count} rows`);
db.close();Transaction benefits:
- All operations succeed or all fail (atomicity)
- Faster than individual transactions
- Cleaner code structure
- Automatic rollback on error
Example with error handling:
const processRowsSafe = db.transaction(() => {
const rows = getRows.all();
for (const row of rows) {
try {
updateRow.run(row.id);
} catch (error) {
console.error(`Failed to process row ${row.id}: ${error.message}`);
throw error; // Rollback the transaction
}
}
return rows.length;
});
try {
const count = processRowsSafe();
console.log(`Success: ${count} rows`);
} catch (error) {
console.error('Transaction failed, all changes rolled back');
}If you're using .iterate() for memory efficiency, refactor to avoid running queries during iteration:
const Database = require('better-sqlite3');
const db = new Database('database.db');
// BAD: Trying to run queries while iterating
// const iterator = db.prepare('SELECT id FROM huge_table').iterate();
// for (const row of iterator) {
// db.prepare('PROCESS ...').run(row.id); // ERROR: connection busy
// }
// GOOD: Collect what you need, then process
const batchSize = 1000;
let offset = 0;
while (true) {
const batch = db
.prepare('SELECT id FROM huge_table LIMIT ? OFFSET ?')
.all(batchSize, offset);
if (batch.length === 0) break;
// Process this batch (now safe to run other queries)
const update = db.prepare('UPDATE huge_table SET processed = 1 WHERE id = ?');
for (const row of batch) {
update.run(row.id);
}
offset += batchSize;
console.log(`Processed: ${offset}`);
}
db.close();Alternative with separate connections:
const Database = require('better-sqlite3');
// Use one connection for reading
const readDb = new Database('database.db');
const writeDb = new Database('database.db');
// Now you can safely iterate on one and write on another
for (const row of readDb.prepare('SELECT id FROM users').iterate()) {
writeDb.prepare('UPDATE users SET status = 1 WHERE id = ?').run(row.id);
}
readDb.close();
writeDb.close();Ensure you're not closing the connection while iterators are still active:
const Database = require('better-sqlite3');
const db = new Database('database.db');
// BAD: Trying to close while iterator is active
// const iterator = db.prepare('SELECT * FROM users').iterate();
// setTimeout(() => db.close(), 100); // ERROR: may close while iterating
// GOOD: Finish iteration before closing
async function processUsers() {
const iterator = db.prepare('SELECT * FROM users').iterate();
const allUsers = [...iterator]; // Consume the entire iterator
db.close(); // Safe to close now
return allUsers;
}
// Or explicitly complete the iterator
const users = [];
const iterator = db.prepare('SELECT * FROM users').iterate();
for (const user of iterator) {
users.push(user);
}
// Iterator is now complete
db.close();Proper resource management:
const Database = require('better-sqlite3');
function withDatabase(callback) {
const db = new Database('database.db');
try {
return callback(db);
} finally {
db.close();
}
}
// Usage
const users = withDatabase((db) => {
return db.prepare('SELECT * FROM users').all();
});
console.log(users);If you absolutely need to read and write simultaneously, use separate database connections:
const Database = require('better-sqlite3');
const readDb = new Database('database.db');
const writeDb = new Database('database.db');
try {
// Safe to iterate on readDb while using writeDb
const selectStmt = readDb.prepare('SELECT id FROM items WHERE active = 0');
const updateStmt = writeDb.prepare('UPDATE items SET active = 1 WHERE id = ?');
for (const row of selectStmt.iterate()) {
updateStmt.run(row.id);
}
} finally {
readDb.close();
writeDb.close();
}Considerations for multiple connections:
- Each connection to the same database file is independent
- Writes are still serialized at the SQLite level (only one writer)
- Reads can happen concurrently
- WAL mode (Write-Ahead Logging) improves concurrency
- Remember to close all connections
With WAL mode:
const Database = require('better-sqlite3');
const readDb = new Database('database.db');
const writeDb = new Database('database.db');
// Enable WAL mode for better concurrency
readDb.pragma('journal_mode = WAL');
try {
// Better concurrency between read and write
const selectStmt = readDb.prepare('SELECT id FROM items LIMIT 100');
const updateStmt = writeDb.prepare('UPDATE items SET status = ? WHERE id = ?');
for (const row of selectStmt.iterate()) {
updateStmt.run('processed', row.id);
}
} finally {
readDb.close();
writeDb.close();
}### Better-sqlite3 Execution Model
Unlike most database libraries, better-sqlite3 is synchronous and single-threaded:
1. Synchronous execution: Each query blocks until complete
2. Single queue per connection: Only one operation at a time
3. No implicit async/await: No hidden concurrency
4. Iterator lifecycle: Iterators must be fully consumed before other operations
This design choice makes:
- Performance predictable
- Code easier to reason about
- Memory usage explicit (you choose .all() vs .iterate())
- Concurrency explicit (use multiple connections if needed)
### Prepared Statements and Iterators
Once you call .iterate() on a prepared statement, the connection is "busy":
const stmt = db.prepare('SELECT * FROM users');
// Starting iteration
const iter = stmt.iterate();
// Connection is now BUSY until iteration completes
// ❌ Can't run this: db.prepare('...').run()
// ❌ Can't run this: db.close()
// Must consume the iterator
for (const row of iter) { /* ... */ }
// NOW connection is free### Transaction Semantics
better-sqlite3 transactions are powerful:
// Manual transaction
db.exec('BEGIN');
try {
// Your operations
db.exec('COMMIT');
} catch {
db.exec('ROLLBACK');
}
// Automatic transaction (recommended)
const myTransaction = db.transaction(() => {
// Automatic BEGIN/COMMIT/ROLLBACK
db.prepare('INSERT ...').run(...);
db.prepare('UPDATE ...').run(...);
});
myTransaction();### Performance Implications
// Slow: Many small transactions
for (const id of ids) {
db.prepare('UPDATE users SET active = 1 WHERE id = ?').run(id);
}
// Each run is a separate transaction
// Fast: One transaction with many operations
const update = db.transaction((ids) => {
const stmt = db.prepare('UPDATE users SET active = 1 WHERE id = ?');
for (const id of ids) {
stmt.run(id);
}
});
update(ids);### Memory vs. Speed Trade-offs
| Method | Memory | Speed | Use Case |
|--------|--------|-------|----------|
| .all() | High (all rows) | Fast (cached) | Small-medium result sets |
| .iterate() | Low (one row) | Slower (per-row overhead) | Large result sets, memory-constrained |
| .get() | Very low (one row) | Fastest | Single row queries |
| .exec() | N/A | Fastest | Multiple statements, no results |
### Connection Management Best Practices
1. Keep connections open for the lifetime of your app
2. Reuse prepared statements (they're cached after first prepare)
3. Use transactions for multiple related operations
4. Don't share connections between threads (sync-only)
5. Always close on app exit or error
6. Enable WAL mode for better concurrency with multiple connections
SQLITE_BUSY: The database file is locked
How to fix 'SQLITE_BUSY: The database file is locked' in SQLite
better-sqlite3: This statement has already been finalized
How to fix "better-sqlite3: This statement has already been finalized" in SQLite
SQLITE_AUTH: Authorization denied
SQLITE_AUTH: Authorization denied
SQLITE_CONSTRAINT_CHECK: CHECK constraint failed
CHECK constraint failed in SQLite
SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified
How to fix "SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified" in SQLite