{"record":{"id":"937c9a969d57c719","repo":"santifer/career-ops","slug":"tracker-transaction-is-already-closed","errorCode":null,"errorMessage":"Tracker transaction is already closed","messagePattern":"Tracker transaction is already closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tracker-utils.mjs","lineNumber":577,"sourceCode":"/**\n * Open one serialized read/replace transaction for an applications tracker.\n * Writers receive only the canonical path plus guarded read and atomic replace\n * operations, keeping the complete mutation inside one shared lock lifetime.\n */\nexport async function openTrackerTransaction(appsFile, options = {}) {\n  const trackerPath = canonicalizeTrackerPath(appsFile);\n  const { lockDir = trackerLockDirFor(trackerPath), ...lockOptions } = options;\n  const lock = await acquireTrackerLock(lockDir, {\n    timeoutMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_TIMEOUT_MS) || 60_000,\n    retryMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_RETRY_MS) || 75,\n    staleMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_STALE_MS) || 10 * 60_000,\n    tracker: trackerPath,\n    ...lockOptions,\n  });\n  let closed = false;\n  let closeError = null;\n  const assertOpen = () => {\n    if (closed) throw new Error('Tracker transaction is already closed');\n  };\n  return {\n    path: trackerPath,\n    read() {\n      assertOpen();\n      return readFileSync(trackerPath, 'utf-8');\n    },\n    replace(content) {\n      assertOpen();\n      writeFileAtomic(trackerPath, content);\n    },\n    close() {\n      if (closed) return closeError;\n      try {\n        lock.release();\n      } catch (err) {\n        closeError = err;\n        console.error(`Warning: tracker transaction closed but lock cleanup failed at ${lockDir}: ${err.message}`);","sourceCodeStart":559,"sourceCodeEnd":595,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/tracker-utils.mjs#L559-L595","documentation":"assertOpen is the guard on every operation of a tracker transaction object (read/write/commit etc.). Once the transaction has been closed (closed=true, typically after commit() or close()), any further method call throws this error. The transaction object is single-use by design.","triggerScenarios":"Calling read(), write(), or any other transaction method after commit()/close() has already run; calling a method twice on a transaction that auto-closes after commit; keeping a reference to the transaction and reusing it in a later code path or retry handler.","commonSituations":"Storing the transaction in a variable used by multiple functions where one commits early; a retry wrapper that retries the whole flow (including commit) after a partial failure; accidentally calling an update helper twice on the same transaction; long-lived module-level transaction reused across requests.","solutions":["Create a new transaction (withTrackerTransaction / the factory used originally) for any further work instead of reusing the closed one","Restructure code so commit() is the last operation and no methods are called afterward","In retry logic, re-create the transaction inside the retry loop rather than reusing it","Log or assert transaction lifecycle in your code to catch double-close/double-use paths"],"exampleFix":"// before\nconst tx = createTrackerTransaction({ tracker });\ntx.commit();\ntx.write(updated);  // throws: already closed\n// after\nconst tx = createTrackerTransaction({ tracker });\ntx.write(updated);\ntx.commit(); // commit last; new tx for new work","handlingStrategy":"try-catch","validationCode":"// track closed state yourself if reusing across scopes\nif (tx.isClosed && tx.isClosed()) throw new Error('create a new transaction');","typeGuard":null,"tryCatchPattern":"try {\n  tx.write(data);\n  tx.commit();\n} catch (e) {\n  if (String(e.message).includes('already closed')) {\n    tx = createTrackerTransaction({ tracker }); // recreate and redo\n  }\n}","preventionTips":["Treat transactions as single-use; scope them tightly with try/finally","Always call commit() last; never call transaction methods after it","Recreate the transaction inside retry loops, not around them","Avoid storing transactions in module-level or long-lived variables"],"tags":["lifecycle","invalid-state","transaction"],"backgroundTag":"invalid-state-transition","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}