{"record":{"id":"3b09200cebd05826","repo":"tursodatabase/turso","slug":"statement-has-been-finalized","errorCode":null,"errorMessage":"Statement has been finalized","messagePattern":"Statement has been finalized","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"bindings/react-native/src/Statement.ts","lineNumber":44,"sourceCode":"  private _finalized = false;\n  private _extraIo?: () => Promise<void>;\n\n  constructor(statement: NativeStatement, connection: NativeConnection, execLock: AsyncLock | null, extraIo?: () => Promise<void>) {\n    this._statement = statement;\n    this._connection = connection;\n    this._execLock = execLock;\n    this._extraIo = extraIo;\n  }\n\n  /**\n   * Bind parameters to the statement\n   *\n   * @param params - Parameters to bind (array, object, or single value)\n   * @returns this for chaining\n   */\n  bind(...params: BindParams[]): this {\n    if (this._finalized) {\n      throw new Error('Statement has been finalized');\n    }\n\n    // Flatten parameters if single array passed\n    let flatParams: SQLiteValue[];\n    if (params.length === 1 && Array.isArray(params[0])) {\n      flatParams = params[0];\n    } else if (params.length === 1 && typeof params[0] === 'object' && params[0] !== null) {\n      // Named parameters\n      const namedParams = params[0] as Record<string, SQLiteValue>;\n      this.bindNamed(namedParams);\n      return this;\n    } else {\n      flatParams = params as SQLiteValue[];\n    }\n\n    // Bind positional parameters\n    this.bindPositional(flatParams);\n    return this;","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/react-native/src/Statement.ts#L26-L62","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nstmt.finalize();\nstmt.bind(42); // throws: finalized\n\n// after\nstmt.finalize();\nstmt = connection.prepare('SELECT ...'); // re-prepare\nstmt.bind(42);","handlingStrategy":"validation","validationCode":"// Own the finalized flag in a thin wrapper\nclass SafeStatement {\n  private finalized = false;\n  constructor(private stmt: Statement) {}\n  bind(...params: BindParams[]) {\n    if (this.finalized) throw new Error('Statement already finalized — re-prepare');\n    this.stmt.bind(...params);\n    return this;\n  }\n  async finalize() { if (!this.finalized) { this.finalized = true; await this.stmt.finalize(); } }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["react-native","statement","finalize","use-after-free"],"backgroundTag":"use-after-free","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}