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
- Declare the transaction handle as the first callback parameter: db.transactionAsync(async (tx, ...args) => { await tx.run(...) })
- 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
- 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
- Type the callback explicitly as (tx: Transaction, ...args: any[]) => any so TypeScript rejects zero-parameter functions at compile time
- Never write a rest-only parameter list for transaction callbacks — fn.length is 0 for (...args)
- Run all transaction SQL through the tx handle; doing so makes forgetting the parameter impossible
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
- Expected first argument to be a function
- Expected first argument to be a function
- Expected first argument to be a function
- No active remote transaction
- Expected first argument to be a function
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/208fbdc70619c38b.
Report an issue: GitHub.