{"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/244cde92a7df7f9b8b8b7a4075c35a12977e303e/bindings/react-native/src/Statement.ts#L26-L62","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst stmt = db.prepare('SELECT * FROM users WHERE id = ?');\nawait stmt.finalize();\n// later:\nstmt.bind(1); // throws 'Statement has been finalized'\n\n// after\nconst stmt = db.prepare('SELECT * FROM users WHERE id = ?');\nawait stmt.finalize();\n// later: prepare fresh\nconst fresh = db.prepare('SELECT * FROM users WHERE id = ?');\nfresh.bind(1);","handlingStrategy":"type-guard","validationCode":"if (stmt.finalized) {\n  throw new Error(`Statement for \"${sql}\" already finalized — re-prepare`);\n}\nstmt.bind(params);","typeGuard":"function isUsableStatement(stmt: Statement): boolean {\n  return !stmt.finalized; // `.finalized` getter exposed by Statement\n}","tryCatchPattern":"try {\n  stmt.bind(params);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Statement has been finalized') {\n    const fresh = db.prepare(sql); // re-prepare and retry once\n    fresh.bind(params);\n    return fresh;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["react-native","statement","lifecycle","finalized"],"backgroundTag":"use-after-free","analyzedSha":"244cde92a7df7f9b8b8b7a4075c35a12977e303e","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}