{"record":{"id":"fd96a981148faa8f","repo":"tursodatabase/turso","slug":"the-transaction-has-already-completed-fd96a9","errorCode":null,"errorMessage":"The transaction has already completed","messagePattern":"The transaction has already completed","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"serverless/javascript/src/connection.ts","lineNumber":605,"sourceCode":"  }\n\n  private async withGate<T>(fn: () => Promise<T>): Promise<T> {\n    await this.gate.acquire();\n    try {\n      return await fn();\n    } finally {\n      this.gate.release();\n    }\n  }\n\n  /** Whether the transaction is still open (COMMIT/ROLLBACK not executed yet). */\n  get open(): boolean {\n    return this.active;\n  }\n\n  private assertActive() {\n    if (!this.active) {\n      throw new TypeError(\"The transaction has already completed\");\n    }\n  }\n\n  /** @internal Marks the transaction completed; called by the wrapper. */\n  finish() {\n    this.active = false;\n  }\n\n  /**\n   * Prepares a SQL statement scoped to the transaction. The statement runs\n   * on the transaction's session without re-acquiring the connection lock\n   * and becomes unusable once the transaction completes.\n   */\n  async prepare(sql: string): Promise<Statement> {\n    const description = await this.withGate(() => this.session.describe(sql));\n    const stmt = Statement.fromSession(this.session, sql, description.cols, this.gate);\n    if (this.defaultSafeIntegerMode) {\n      stmt.safeIntegers(true);","sourceCodeStart":587,"sourceCodeEnd":623,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/serverless/javascript/src/connection.ts#L587-L623","documentation":"Thrown by every method on the Transaction handle after the transaction has committed or rolled back: the transactionAsync wrapper calls txn.finish() in its finally block, which flips the active flag, and every handle method goes through assertActive()/the gate. The dedicated session is closed at the same moment, so statements prepared from the handle are equally unusable.","triggerScenarios":"Capturing tx in a closure or class field and calling tx.run()/tx.all()/tx.prepare()/tx.batch()/tx.exec() after the callback has returned; fire-and-forget promises started inside the callback that outlive the COMMIT and later touch tx; executing a Statement obtained from tx.prepare() after the transaction completes.","commonSituations":"Queueing background work inside a transaction that still references the handle; retry wrappers that accidentally reuse the finished handle; generators created inside the callback being consumed after it ends.","solutions":["Move every statement that uses tx inside the transactionAsync callback, before it returns","Await all promises created inside the callback so no work leaks past COMMIT/ROLLBACK","Pass plain data (ids, row values) to post-transaction work — never the tx handle or statements made from it","Check the tx.open property before use to fail fast with your own message"],"exampleFix":"// before\nlet txRef: any;\nawait db.transactionAsync(async (tx) => { txRef = tx; await tx.run(\"INSERT INTO t VALUES (1)\"); })();\nawait txRef.run(\"UPDATE t SET x = 2\"); // TypeError: transaction already completed\n\n// after\nawait db.transactionAsync(async (tx) => {\n  await tx.run(\"INSERT INTO t VALUES (1)\");\n  await tx.run(\"UPDATE t SET x = 2\");\n})(); // all SQL inside the callback","handlingStrategy":"type-guard","validationCode":"if (!tx.open) {\n  throw new Error(\"transaction finished — restart the transactionAsync call\");\n}\nawait tx.run(\"UPDATE t SET x = 1\");","typeGuard":"const isTxUsable = (tx: { open: boolean }): boolean => tx.open;\n// `open` is false once the wrapper ran COMMIT/ROLLBACK; check it before\n// every late (closure, deferred, retried) use of the handle.","tryCatchPattern":"try {\n  await tx.run(sql);\n} catch (e) {\n  if (e instanceof TypeError && e.message === \"The transaction has already completed\") {\n    // Do not reuse the handle — rerun the whole transactionAsync(fn) call.\n  } else throw e;\n}","preventionTips":["Keep every tx.* call inside the callback body and await all promises before returning","Pass plain data to post-commit work — never the tx handle or statements created from it","When this fires, search for the handle escaping via closures, class fields, or floating promises"],"tags":["transaction","lifecycle","async","javascript"],"backgroundTag":"transaction-used-after-commit","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}