tursodatabase/turso · error · Error
Statement has been finalized
Error message
Statement has been finalized
What it means
bind() refuses to operate once _finalized is true. A Statement wraps a native prepared-statement handle; finalize() releases it (Statement.ts:398-427), after which the handle is dead. The flag is checked on every entry point so you fail fast in JS instead of dereferencing a freed native statement.
Solutions
- Re-prepare the statement when needed: const stmt = db.prepare(sql) returns a fresh Statement each time
- Only finalize when you are certain no further calls will reference the statement — finalize after the last awaited use
- Check the `.finalized` getter before reusing a cached Statement
- Keep the common pattern: one-shot `await db.all(sql, params)` / `db.prepare(sql).run(params)` instead of long-lived statement objects
Example fix
// before
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
await stmt.finalize();
// later:
stmt.bind(1); // throws 'Statement has been finalized'
// after
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
await stmt.finalize();
// later: prepare fresh
const fresh = db.prepare('SELECT * FROM users WHERE id = ?');
fresh.bind(1); Defensive patterns
Strategy: type-guard
Validate before calling
if (stmt.finalized) {
throw new Error(`Statement for "${sql}" already finalized — re-prepare`);
}
stmt.bind(params); Type guard
function isUsableStatement(stmt: Statement): boolean {
return !stmt.finalized; // `.finalized` getter exposed by Statement
} Try / catch
try {
stmt.bind(params);
} catch (e) {
if (e instanceof Error && e.message === 'Statement has been finalized') {
const fresh = db.prepare(sql); // re-prepare and retry once
fresh.bind(params);
return fresh;
}
throw e;
} Prevention
- Prefer one-shot APIs (db.all/db.get/db.run with SQL strings) over long-lived Statement objects
- Call finalize() only at teardown, after every awaited use has settled
- Invalidate statement caches when entries are finalized
When it happens
Trigger: Calling stmt.bind(...) (or passing params to a finalized statement's run/get/all, which call bind internally) after `await stmt.finalize()`; reusing a statement object whose finalize ran in a finally block or cleanup handler.
Common situations: Cleanup code that finalizes statements eagerly (e.g. on unmount) while queued queries still hold the statement; loops that finalize inside the iteration but continue reusing the object; wrapping statements in caches without invalidating on finalize.
Related errors
- Database is closed
- Database not connected. Call connect() first.
- getAllRows failed with status
- No connection available
- Statement execution failed with status
AI-assisted analysis of tursodatabase/turso@244cde92a7 (2026-08-20).
Data as JSON: /api/errors/3b09200cebd05826.
Report an issue: GitHub.
Appendix: 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 244cde92a7)