tursodatabase/turso · error · TypeError

Expected first argument to be a function

Error message

Expected first argument to be a function

What it means

The vite promise wrapper's transaction() validates that its first argument is callable before dispatching — either to the remote-writer path (beginTransaction/commit around the callback) or to the base promise implementation. Passing anything that is not a function throws an immediate TypeError. This mirrors the standard better-sqlite3/libsql ergonomic where transaction() wraps a user callback.

Source

Thrown at bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts:282

        const isReadonly = category === "read";
        return new RemoteWriteStatement(
            localStmt,
            sql,
            isReadonly,
            this.#remoteWriter,
            () => this.pull(),
        ) as any;
    }

    /**
     * Returns a function that executes the given function in a transaction.
     * When remoteWrites is enabled, the entire transaction goes to remote.
     */
    override transaction<F extends (...args: any[]) => Promise<any>>(
        fn: F,
    ): TransactionFunction<F> {
        if (typeof fn !== "function")
            throw new TypeError("Expected first argument to be a function");

        if (!this.#remoteWriter) {
            return super.transaction(fn);
        }

        const db = this;
        const remoteWriter = this.#remoteWriter;
        const wrapTxn = (mode: string) => {
            return async (...bindParameters: any[]) => {
                await remoteWriter.beginTransaction(mode);
                try {
                    const result = await fn(...bindParameters);
                    await remoteWriter.commitTransaction();
                    await db.pull();
                    return result;
                } catch (err) {
                    await remoteWriter.rollbackTransaction();
                    throw err;

View on GitHub (pinned to bad083fafb)

Solutions

  1. Pass the function itself, not its result: `db.transaction(fn)` not `db.transaction(fn())`
  2. If the callback may be missing, default it or check `typeof fn === "function"` before calling transaction()
  3. Let TypeScript catch it: type the parameter as `(...args: any[]) => Promise<any>` and fix the compile error at the call site

Example fix

// before
const tx = db.transaction(saveRecord()); // invokes saveRecord, passes its result
tx(params);

// after
const tx = db.transaction(saveRecord); // passes the function itself
tx(params);
Defensive patterns

Strategy: type-guard

Type guard

function isAsyncFn<F>(fn: unknown): fn is (...args: any[]) => Promise<any> {
  return typeof fn === 'function' && fn.constructor?.name !== 'AsyncFunction' || typeof fn === 'function';
}
// simpler and sufficient:
const isTxnFn = (f: unknown): f is (...args: any[]) => Promise<any> => typeof f === 'function';

if (!isTxnFn(fn)) throw new TypeError(`transaction() needs a function, got ${typeof fn}`);
const tx = db.transaction(fn);

Try / catch

try { tx = db.transaction(fn); } catch (e) { if (e instanceof TypeError && /first argument/i.test(e.message)) { /* fix the call site — wrong argument */ } throw e; }

Prevention

When it happens

Trigger: `db.transaction()` with no arguments; `db.transaction(myFn())` which calls the function and passes its return value; passing an arrow stored in a possibly-undefined variable (`db.transaction(this.handlers.save)` where the property does not exist); passing an object or string instead of a callback.

Common situations: Refactoring that renames or removes the callback but leaves the call site; dynamically selecting a callback from a map where the key is missing; copy-pasting between APIs where a SQL string is passed instead of a function; optional-chaining slips that yield undefined.

Related errors


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