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
- Declare the handle first: db.transactionAsync(async (tx, ...args) => { await tx.run(...) })
- 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
- Always write transactionAsync callbacks as async (tx, ...args) => {}
- Configure ESLint's unused-args rule (argsIgnorePattern: '^_') so the tx parameter is never auto-deleted
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
- Expected first argument to be a function
- Expected first argument to be a function
- Expected first argument to be a function
- Expected first argument to be a function
- Expected first argument to be an array of statements
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)