{"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":395,"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":377,"sourceCodeEnd":413,"githubUrl":"https://github.com/tursodatabase/turso/blob/6c7252267988c76e632af00a671e4b9788dfae13/serverless/javascript/src/compat.ts#L377-L413","documentation":"Inside a transaction, every operation funnels through ensureOpen(), which throws LibsqlError code TRANSACTION_CLOSED once txClosed is set - which happens as soon as commit(), rollback() or close() has run (closeTx sets it before closing the session). The transaction object deliberately keeps working as an object but refuses further SQL: one transaction equals one unit of work.","triggerScenarios":"Calling tx.execute(...) or tx.batch(...) after an earlier await tx.commit() / tx.rollback() / tx.close(); classic double-close shape: rollback in a catch plus commit/rollback again in a finally; storing the tx object and reusing it for the next logical operation.","commonSituations":"finally { await tx.rollback(); } combined with an explicit rollback in the error path - the second call throws and masks the original error; sequential business operations mistakenly sharing one transaction handle instead of opening a new one.","solutions":["Call exactly one of commit/rollback/close per transaction, and track it with a flag or use try/commit + catch/rollback without a finally-close","Open a fresh transaction with client.transaction() for each new unit of work","Check the tx.closed getter on the Transaction object before issuing statements","In error paths, let the catch do rollback and make finally a no-op, so a second close cannot throw over the real error"],"exampleFix":"// before - double close: rollback in catch AND finally\ntry {\n  await tx.execute(insert);\n  await tx.commit();\n} catch (e) {\n  await tx.rollback();\n} finally {\n  await tx.rollback(); // throws TRANSACTION_CLOSED, masks the real error\n}\n\n// after - exactly one terminator per transaction\ntry {\n  await tx.execute(insert);\n  await tx.commit();\n} catch (e) {\n  if (!tx.closed) await tx.rollback();\n}","handlingStrategy":"type-guard","validationCode":"if (!tx.closed) {\n  await tx.execute(stmt);\n}","typeGuard":"function isTxOpen(tx: Transaction): boolean {\n  return !tx.closed;\n}","tryCatchPattern":"catch (e) { if (e instanceof LibsqlError && e.code === 'TRANSACTION_CLOSED') { const fresh = await client.transaction(mode); return fresh.execute(stmt); // restart the unit of work on a new tx } throw e; }","preventionTips":["Terminate each transaction exactly once: commit in try, rollback in catch, nothing in finally","Check tx.closed before issuing statements on stored transaction handles","Open a new transaction per unit of work - never reuse a committed/rolled-back handle"],"tags":["serverless","transaction","use-after-close","libsql-compat"],"backgroundTag":"transaction-used-after-close","analyzedSha":"6c7252267988c76e632af00a671e4b9788dfae13","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}