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

  1. Do not touch a statement after finalize(); prepare a new one from the connection when needed again
  2. Remove finalize() from cleanup paths that run before all users are done — finalize only when the statement truly leaves scope
  3. 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

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


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/3b09200cebd05826. Report an issue: GitHub.