tursodatabase/turso · error · TypeError

transactionAsync() callbacks receive a Transaction handle as

Error message

transactionAsync() callbacks receive a Transaction handle as their first argument and must declare it: db.transactionAsync(async (tx, ...args) => { await tx.run(...) }).

What it means

Thrown by Connection.transactionAsync() when the callback declares zero parameters (fn.length === 0). transactionAsync() runs the callback on a dedicated server stream and passes a Transaction handle as the first argument; all transaction SQL must go through that handle, so a callback that ignores it would silently run SQL on the connection's own stream, outside the transaction. The library rejects such callbacks up front to make that bug impossible.

Source

Thrown at serverless/javascript/src/connection.ts:474

   *
   * @example
   * ```typescript
   * const insertMany = client.transactionAsync(async (tx, users) => {
   *   const insert = await tx.prepare("INSERT INTO users (name) VALUES (?)");
   *   for (const user of users) {
   *     await insert.run([user]);
   *   }
   * });
   *
   * await insertMany(['Alice', 'Bob', 'Charlie']);
   * ```
   */
  transactionAsync(fn: (tx: Transaction, ...args: any[]) => any): any {
    if (typeof fn !== "function") {
      throw new TypeError("Expected first argument to be a function");
    }
    if (fn.length === 0) {
      throw new TypeError(
        "transactionAsync() callbacks receive a Transaction handle as their first argument " +
        "and must declare it: db.transactionAsync(async (tx, ...args) => { await tx.run(...) }).",
      );
    }

    const db = this;
    const wrapTxn = (mode: string) => {
      return async (...bindParameters: any[]) => {
        if (!db.isOpen) {
          throw new TypeError("The database connection is not open");
        }
        // The transaction owns a dedicated session (server stream), so the
        // connection is not locked for its duration: concurrent statements
        // on the connection run on its own stream, outside the transaction.
        const session = new Session(db.config);
        const txn = new Transaction(session, db.defaultSafeIntegerMode);
        try {
          await txn.exec("BEGIN " + mode);

View on GitHub (pinned to bad083fafb)

Solutions

  1. Declare the transaction handle as the first callback parameter: db.transactionAsync(async (tx, ...args) => { await tx.run(...) })
  2. Run all transaction SQL through the tx handle (tx.run/tx.get/tx.all/tx.prepare/tx.batch), never through db — Connection calls execute outside the transaction and cannot see its uncommitted writes
  3. If you did not want a transaction-scoped callback, call db.run()/db.all() directly instead of transactionAsync()

Example fix

// before
const txn = db.transactionAsync(async () => {
  await db.run("INSERT INTO users(name) VALUES ('Alice')"); // runs OUTSIDE the transaction
});

// after
const txn = db.transactionAsync(async (tx) => {
  await tx.run("INSERT INTO users(name) VALUES ('Alice')"); // inside the transaction
});
await txn.immediate();
Defensive patterns

Strategy: validation

Validate before calling

const declaresFirstParam = (fn: unknown): boolean =>
  typeof fn === "function" && fn.length > 0;

if (!declaresFirstParam(callback)) {
  throw new Error("transactionAsync callback must declare (tx, ...args)");
}
const txn = db.transactionAsync(callback);

Type guard

function isTxCallback(
  fn: unknown,
): fn is (tx: any, ...args: any[]) => any {
  // fn.length counts parameters before the first default/rest,
  // so (tx, ...args) => {} passes and (...args) => {} or () => {} fails.
  return typeof fn === "function" && fn.length > 0;
}

Prevention

When it happens

Trigger: Calling db.transactionAsync(async () => {...}) or db.transactionAsync(function() {...}) — any callback with an empty parameter list. Note that rest-only signatures like (...args) => {...} also have fn.length === 0 and are rejected, while (tx, ...args) => {...} passes.

Common situations: Code migrated from the deprecated transaction(), whose callback receives no tx argument; IDEs or linters removing an 'unused' parameter; passing a zero-arg wrapper like () => runWork(db) that uses the outer Connection instead of the transaction handle.

Related errors


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