tursodatabase/turso · error · Error
Statement has been finalized
Error message
Statement has been finalized
What it means
The React Native Statement wrapper sets _finalized when finalize() runs, and every subsequent bind() checks it first and throws. A finalized statement's underlying native statement has been released, so rebinding would use freed resources; the binding rejects it instead. This is the classic use-after-free pattern known from better-sqlite3's 'The statement has been finalized'.
Source
Thrown at bindings/react-native/src/Statement.ts:44
private _finalized = false;
private _extraIo?: () => Promise<void>;
constructor(statement: NativeStatement, connection: NativeConnection, execLock: AsyncLock | null, extraIo?: () => Promise<void>) {
this._statement = statement;
this._connection = connection;
this._execLock = execLock;
this._extraIo = extraIo;
}
/**
* Bind parameters to the statement
*
* @param params - Parameters to bind (array, object, or single value)
* @returns this for chaining
*/
bind(...params: BindParams[]): this {
if (this._finalized) {
throw new Error('Statement has been finalized');
}
// Flatten parameters if single array passed
let flatParams: SQLiteValue[];
if (params.length === 1 && Array.isArray(params[0])) {
flatParams = params[0];
} else if (params.length === 1 && typeof params[0] === 'object' && params[0] !== null) {
// Named parameters
const namedParams = params[0] as Record<string, SQLiteValue>;
this.bindNamed(namedParams);
return this;
} else {
flatParams = params as SQLiteValue[];
}
// Bind positional parameters
this.bindPositional(flatParams);
return this;View on GitHub (pinned to bad083fafb)
Solutions
- Do not touch a statement after finalize(); prepare a new one from the connection when needed again
- Remove finalize() from cleanup paths that run before all users are done — finalize only when the statement truly leaves scope
- If you cache statements, evict references from the cache at the same moment you finalize so no stale handle survives
Example fix
// before
stmt.finalize();
stmt.bind(42); // throws: finalized
// after
stmt.finalize();
stmt = connection.prepare('SELECT ...'); // re-prepare
stmt.bind(42); Defensive patterns
Strategy: validation
Validate before calling
// Own the finalized flag in a thin wrapper
class SafeStatement {
private finalized = false;
constructor(private stmt: Statement) {}
bind(...params: BindParams[]) {
if (this.finalized) throw new Error('Statement already finalized — re-prepare');
this.stmt.bind(...params);
return this;
}
async finalize() { if (!this.finalized) { this.finalized = true; await this.stmt.finalize(); } }
} Prevention
- Never call bind()/run()/get()/all() after finalize() — re-prepare instead
- Evict statements from caches at finalize time so no stale handles remain
- Keep finalize() in the same scope that created the statement
When it happens
Trigger: Calling `stmt.bind(...)` after `await stmt.finalize()`; reusing a statement kept in a cache/map that was finalized by cleanup code; a loop that finalizes on error but continues to the next iteration using the same statement.
Common situations: Statement caches with aggressive eviction that finalize while other code still holds references; error paths that finalize in a finally block and then retry with the same object; refactoring from per-use prepare to cached statements (or back) without updating finalize placement.
Related errors
- Unknown parameter name: ${name}
- Unsupported parameter type: ${typeof value}
- database must be connected before execution the function
- Turso native module not loaded
- push() is only available for sync databases
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/3b09200cebd05826.
Report an issue: GitHub.