santifer/career-ops · error
Tracker transaction is already closed
Error message
Tracker transaction is already closed
What it means
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.
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
Example fix
// before
const tx = createTrackerTransaction({ tracker });
tx.commit();
tx.write(updated); // throws: already closed
// after
const tx = createTrackerTransaction({ tracker });
tx.write(updated);
tx.commit(); // commit last; new tx for new work Defensive patterns
Strategy: try-catch
Validate before calling
// track closed state yourself if reusing across scopes
if (tx.isClosed && tx.isClosed()) throw new Error('create a new transaction'); Try / catch
try {
tx.write(data);
tx.commit();
} catch (e) {
if (String(e.message).includes('already closed')) {
tx = createTrackerTransaction({ tracker }); // recreate and redo
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/937c9a969d57c719.
Report an issue: GitHub.
Appendix: source
Thrown at tracker-utils.mjs:577
/**
* Open one serialized read/replace transaction for an applications tracker.
* Writers receive only the canonical path plus guarded read and atomic replace
* operations, keeping the complete mutation inside one shared lock lifetime.
*/
export async function openTrackerTransaction(appsFile, options = {}) {
const trackerPath = canonicalizeTrackerPath(appsFile);
const { lockDir = trackerLockDirFor(trackerPath), ...lockOptions } = options;
const lock = await acquireTrackerLock(lockDir, {
timeoutMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_TIMEOUT_MS) || 60_000,
retryMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_RETRY_MS) || 75,
staleMs: Number(process.env.CAREER_OPS_TRACKER_LOCK_STALE_MS) || 10 * 60_000,
tracker: trackerPath,
...lockOptions,
});
let closed = false;
let closeError = null;
const assertOpen = () => {
if (closed) throw new Error('Tracker transaction is already closed');
};
return {
path: trackerPath,
read() {
assertOpen();
return readFileSync(trackerPath, 'utf-8');
},
replace(content) {
assertOpen();
writeFileAtomic(trackerPath, content);
},
close() {
if (closed) return closeError;
try {
lock.release();
} catch (err) {
closeError = err;
console.error(`Warning: tracker transaction closed but lock cleanup failed at ${lockDir}: ${err.message}`);View on GitHub (pinned to e7abd431fc)