{"record":{"id":"93aa9c04aa2c59f5","repo":"tursodatabase/turso","slug":"transaction-closed","errorCode":"TRANSACTION_CLOSED","errorMessage":"Transaction is closed","messagePattern":"Transaction is closed","errorType":"exception","errorClass":"LibsqlError","httpStatus":null,"severity":"error","filePath":"serverless/javascript/src/compat.ts","lineNumber":386,"sourceCode":"        return \"BEGIN\";\n    }\n  }\n\n  async transaction(mode?: TransactionMode): Promise<Transaction> {\n    await this.execLock.acquire();\n\n    if (this._closed) {\n      this.execLock.release();\n      throw new LibsqlError(\"Client is closed\", \"CLIENT_CLOSED\");\n    }\n\n    const txSession = new Session(this.sessionConfig);\n    let txClosed = false;\n    let cleanupStarted = false;\n\n    const ensureOpen = () => {\n      if (txClosed) {\n        throw new LibsqlError(\"Transaction is closed\", \"TRANSACTION_CLOSED\");\n      }\n    };\n\n    const closeTx = async () => {\n      if (cleanupStarted) return;\n      cleanupStarted = true;\n      txClosed = true;\n      try {\n        await txSession.close();\n      } finally {\n        this.execLock.release();\n      }\n    };\n\n    const executeInTx = async (stmt: InStatement): Promise<ResultSet> => {\n      ensureOpen();\n      const normalized = this.normalizeStatement(stmt);\n      try {","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/serverless/javascript/src/compat.ts#L368-L404","documentation":"Inside the transaction handle returned by the compat client's transaction(), every operation calls ensureOpen(), which throws a LibsqlError with code TRANSACTION_CLOSED when the internal txClosed flag is set. txClosed is flipped by closeTx(), which runs on commit(), rollback(), or close(), so the handle is intentionally single-shot: once the transaction finishes, its session is gone and the handle is dead.","triggerScenarios":"Using tx.execute/tx.batch after awaiting tx.commit() or tx.rollback(). Double-committing (calling commit twice). Keeping the tx handle in a closure or variable and using it after the transaction body completes. Calling rollback in a catch block and then falling through to more tx calls.","commonSituations":"Control flow that continues after an early commit; error handlers that roll back and then code paths accidentally run more statements; storing the tx handle for later use (e.g. in a class field) instead of finishing all work inside the transaction scope.","solutions":["Perform all statements before commit()/rollback() — the handle dies as soon as the transaction ends","Return early or restructure control flow so no code path reaches tx.* after a commit or rollback","Check the public tx.closed getter before late operations","If you need more work done, open a new transaction: await client.transaction(mode)"],"exampleFix":"// before\nawait tx.execute({ sql: 'INSERT INTO a VALUES (1)' });\nawait tx.commit();\nawait tx.execute({ sql: 'INSERT INTO b VALUES (1)' }); // TRANSACTION_CLOSED\n\n// after\nawait tx.execute({ sql: 'INSERT INTO a VALUES (1)' });\nawait tx.execute({ sql: 'INSERT INTO b VALUES (1)' });\nawait tx.commit();","handlingStrategy":"validation","validationCode":"async function txStep(tx: Transaction, stmt: InStatement) {\n  if (tx.closed) {\n    throw new Error('transaction already committed/rolled back — open a new one');\n  }\n  return tx.execute(stmt);\n}","typeGuard":"const txIsOpen = (tx: Transaction): boolean => !tx.closed;","tryCatchPattern":"try {\n  await tx.execute(stmt);\n} catch (e) {\n  if (e instanceof LibsqlError && e.code === 'TRANSACTION_CLOSED') {\n    // Do not retry on the dead handle — restart the whole transaction if needed\n    return runInNewTransaction(stmt);\n  }\n  throw e;\n}","preventionTips":["Keep all statements before commit()/rollback() and return immediately after ending the transaction","Never store the tx handle beyond the transaction scope (no class fields, no background queues)","Check the public tx.closed getter when control flow makes completion state unclear","In catch blocks that roll back, ensure no subsequent code path uses tx again"],"tags":["transaction","lifecycle","transaction-closed","use-after-close"],"backgroundTag":"use-after-close","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}