tursodatabase/turso · error · Error
No active remote transaction
Error message
No active remote transaction
What it means
RemoteWriter.commitTransaction() requires an open session created by beginTransaction(); the session field doubles as the in-remote-transaction flag. If commit is attempted with no session — no BEGIN sent, or a previous COMMIT/ROLLBACK already closed and nulled the session — it throws immediately without touching the network. The finally block guarantees the session is always cleared after the first commit/rollback, which is why double-commit hits this path.
Source
Thrown at bindings/javascript/sync/packages/common/remote-writer.ts:90
await session.close();
}
}
/**
* Begin a remote transaction. Creates a session and sends BEGIN.
*/
async beginTransaction(mode: string): Promise<void> {
this.session = await this.createSession();
await this.session.sequence("BEGIN " + mode);
this._inRemoteTxn = true;
}
/**
* Commit the remote transaction. Sends COMMIT and closes the session.
*/
async commitTransaction(): Promise<void> {
if (!this.session) {
throw new Error("No active remote transaction");
}
try {
await this.session.sequence("COMMIT");
} finally {
this._inRemoteTxn = false;
await this.session.close();
this.session = null;
}
}
/**
* Rollback the remote transaction. Sends ROLLBACK and closes the session.
*/
async rollbackTransaction(): Promise<void> {
if (!this.session) {
throw new Error("No active remote transaction");
}
try {View on GitHub (pinned to bad083fafb)
Solutions
- Only commit after a successful beginTransaction(); check writer.isInTransaction first.
- In finally blocks, guard the commit: if (!writer.isInTransaction) skip it.
- Make BEGIN failures abort before the callback runs, so COMMIT is never attempted.
- Serialize access so only one code path ends a given remote transaction.
Example fix
// before
await writer.beginTransaction('DEFERRED');
try { await doWork(); } finally { await writer.commitTransaction(); } // BEGIN failure -> commit throws too
// after
await writer.beginTransaction('DEFERRED');
let ok = false;
try { await doWork(); ok = true; }
finally {
if (ok) await writer.commitTransaction();
else await writer.rollbackTransaction().catch(() => {});
} Defensive patterns
Strategy: validation
Validate before calling
if (!writer.isInTransaction) {
throw new Error('cannot commit: BEGIN never sent or session already closed');
}
await writer.commitTransaction(); Try / catch
try { await writer.commitTransaction(); } catch (e) { if (e instanceof Error && e.message === 'No active remote transaction') { /* already ended; treat as no-op */ } else throw e; } Prevention
- Commit exactly once per successful beginTransaction().
- Never put commitTransaction() in a finally that also runs when BEGIN failed.
- Model remote transaction state explicitly in your wrapper instead of inferring it.
When it happens
Trigger: Calling writer.commitTransaction() before any beginTransaction(); calling commit twice (the second has session === null); beginning failed midway so the session was never assigned; calling rollback after commit.
Common situations: Transaction wrappers that always issue COMMIT in a finally block even when BEGIN threw; retry logic that replays COMMIT; parallel flows both trying to end the same remote transaction; error paths where BEGIN failed on the server.
Related errors
- reader is null
- remoteWritesExperimental requires a non-null URL
- sync is disabled as database was opened without sync support
- Expected first argument to be a function
- transactionAsync is not supported with remoteWritesExperimen
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/6ca581a10a14fc15.
Report an issue: GitHub.