tursodatabase/turso · error · Error

transactionAsync is not supported with remoteWritesExperimen

Error message

transactionAsync is not supported with remoteWritesExperimental yet; use the deprecated transaction() for now

What it means

transactionAsync() runs its callback on a connection owned for the whole BEGIN..COMMIT window and hands it a Transaction handle. When the Database was constructed with remoteWritesExperimental, transactions execute on the remote server and there is no local connection to hand out, so the override throws immediately instead of silently running the transaction locally. The deprecated transaction() is the supported path under remote writes: its wrapper routes BEGIN, the statements and COMMIT to the remote as one transaction.

Source

Thrown at bindings/javascript/sync/packages/wasm/promise-default.ts:333

        Object.defineProperties(properties.immediate.value, properties);
        Object.defineProperties(properties.exclusive.value, properties);
        return properties.default.value as TransactionFunction<F>;
    }

    /**
     * Returns a function that executes the given function in a transaction
     * on a connection owned for the whole BEGIN..COMMIT window; the callback
     * receives a {@link Transaction} handle as its first argument.
     *
     * Not supported together with {@link DatabaseOpts.remoteWritesExperimental}
     * yet: remote-writes transactions run on the remote server and have no
     * local connection to hand out.
     */
    override transactionAsync<F extends (txn: Transaction, ...args: any[]) => Promise<any>>(
        fn: F,
    ): AsyncTransactionFunction<F> {
        if (this.#remoteWriter) {
            throw new Error(
                "transactionAsync is not supported with remoteWritesExperimental yet; use the deprecated transaction() for now",
            );
        }
        return super.transactionAsync(fn);
    }

    /**
     * close the database and relevant files
     */
    async close() {
        if (this.#remoteWriter) {
            await this.#remoteWriter.close();
        }
        if (this.#engine != null) {
            if (this.name != null && this.#worker != null) {
                await Promise.all([
                    unregisterFileAtWorker(this.#worker, this.name),
                    unregisterFileAtWorker(this.#worker, `${this.name}-wal`),

View on GitHub (pinned to bad083fafb)

Solutions

  1. Replace transactionAsync(fn) with transaction(fn): with remoteWritesExperimental enabled its wrapper sends BEGIN, the statements and COMMIT to the remote server as a single transaction.
  2. If you need the interactive Transaction-handle semantics (held local connection), disable remoteWritesExperimental and keep writes local, syncing via push().
  3. Alternatively express the unit of work as awaited exec/run calls — with remote writes enabled, write statements are forwarded to the remote.

Example fix

// before — throws: no local connection exists to hand out under remote writes
const transfer = db.transactionAsync(async (txn, from, to, amount) => {
    await txn.exec({ sql: "UPDATE accounts SET bal = bal - ? WHERE id = ?", args: [amount, from] });
    await txn.exec({ sql: "UPDATE accounts SET bal = bal + ? WHERE id = ?", args: [amount, to] });
});
await transfer(1, 2, 50);

// after — deprecated transaction() is the supported API with remoteWritesExperimental
const transfer = db.transaction(async (from: number, to: number, amount: number) => {
    await db.exec({ sql: "UPDATE accounts SET bal = bal - ? WHERE id = ?", args: [amount, from] });
    await db.exec({ sql: "UPDATE accounts SET bal = bal + ? WHERE id = ?", args: [amount, to] });
});
await transfer(1, 2, 50); // whole BEGIN..COMMIT goes to the remote server
Defensive patterns

Strategy: fallback

Validate before calling

const remoteWrites = process.env.TURSO_REMOTE_WRITES === "1";
const db = new Database({ path, url, remoteWritesExperimental: remoteWrites });

// Choose the transaction API to match the mode BEFORE calling either method:
const makeTx = remoteWrites
    ? <F extends (...a: any[]) => Promise<any>>(fn: F) => db.transaction(fn)
    : <F extends (txn: Transaction, ...a: any[]) => Promise<any>>(fn: F) => db.transactionAsync(fn);

Type guard

function supportsTransactionAsync(remoteWritesEnabled: boolean): boolean {
    // transactionAsync is only valid without remoteWritesExperimental
    return !remoteWritesEnabled;
}

Try / catch

let runTx: (...args: unknown[]) => Promise<unknown>;
try {
    runTx = db.transactionAsync(fn);
} catch (e) {
    if (e instanceof Error && e.message.includes("remoteWritesExperimental")) {
        runTx = db.transaction(fn); // documented fallback for remote-write mode
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Constructing with { url, remoteWritesExperimental: true } — which sets this.#remoteWriter — and then calling db.transactionAsync(fn). The guard is the first statement of the override, so it throws before fn is ever touched.

Common situations: Enabling remote writes in an existing codebase that already used transactionAsync; upgrading @tursodatabase/sync-wasm where the restriction is new; mixing sync-local and remote-write modes behind a runtime flag where only one branch was migrated to transaction().

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/309f6a1a6653576d. Report an issue: GitHub.