tursodatabase/turso · error · TypeError

transactionAsync() callbacks receive a Transaction handle…

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

`transactionAsync()` inspects the callback's declared arity (`fn.length`) and rejects zero-parameter callbacks. The first declared parameter receives the Transaction handle that owns a dedicated session; a callback that declares no parameters cannot use it, so it is refused rather than silently running statements on the wrong session. Note that rest-only arrows like `(...args) => {}` also report length 0.

Solutions

  1. Declare the handle first: db.transactionAsync(async (tx, ...args) => { await tx.run(...) })
  2. Configure your linter to ignore unused function parameters (argsIgnorePattern) so `tx` survives

Example fix

// before
await db.transactionAsync(async () => {
  await db.run('INSERT INTO t VALUES (1)');
})();
// after
await db.transactionAsync(async (tx) => {
  await tx.run('INSERT INTO t VALUES (1)');
})();
Defensive patterns

Strategy: validation

Validate before calling

if (typeof fn !== 'function' || fn.length === 0) {
  throw new Error('transactionAsync callback must declare a tx parameter: async (tx, ...args) => {}');
}
await db.transactionAsync(fn).immediate();

Type guard

const declaresTxParam = (fn: unknown): fn is (tx: Transaction, ...args: any[]) => any =>
  typeof fn === 'function' && fn.length > 0;

Prevention

When it happens

Trigger: `db.transactionAsync(async () => { ... })`; `db.transactionAsync(async (...args) => { ... })` - both declare zero required leading parameters; a linter auto-removing the unused `tx` parameter.

Common situations: Converting synchronous transaction() code whose callback took no handle; strict unused-parameter lint rules deleting the tx binding; copy-paste between the sync and async transaction APIs.

Related errors


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

Appendix: source

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

   *
   * @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 c1e5928725)