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

  1. Re-prepare the statement when needed: const stmt = db.prepare(sql) returns a fresh Statement each time
  2. Only finalize when you are certain no further calls will reference the statement — finalize after the last awaited use
  3. Check the `.finalized` getter before reusing a cached Statement
  4. 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

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


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)