tursodatabase/turso · error · TypeError

Expected first argument to be an array of statements

Error message

Expected first argument to be an array of statements

What it means

Connection.batch() validates its first argument with Array.isArray and throws a plain TypeError when it is not an array. Notably this check runs before the isOpen check, so a non-array argument raises this TypeError even on a closed connection. It mirrors the compat layer and better-sqlite3 convention that batch takes only arrays of statements.

Source

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

   *
   * @example
   * // Atomic via the mode parameter.
   * await db.batch([
   *   { sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] },
   *   { sql: "INSERT INTO users(name) VALUES (?)", args: ["Frank"] },
   * ], "immediate");
   *
   * @example
   * // Atomic via the transactionAsync() API for mixed workloads.
   * const txn = db.transactionAsync(async (tx) => {
   *   await tx.batch([{ sql: "INSERT INTO users(name) VALUES (?)", args: ["Eve"] }]);
   *   await tx.run("UPDATE counters SET n = n + 1");
   * });
   * await txn.immediate();
   */
  async batch(statements: BatchStatement[], options?: BatchMode | BatchOptions, queryOptions?: QueryOptions): Promise<any> {
    if (!Array.isArray(statements)) {
      throw new TypeError("Expected first argument to be an array of statements");
    }
    if (!this.isOpen) {
      throw new TypeError("The database connection is not open");
    }
    await this.execLock.acquire();
    try {
      const { mode, raw } = normalizeBatchOptions(options);
      // Inside an outer transaction(...) callback the surrounding BEGIN
      // already opened a transaction on this stream; emitting another
      // `BEGIN` step would fail, so ignore the user-supplied mode.
      const effectiveMode = this.session.inTransaction ? undefined : mode;
      const results = await this.session.batch(
        statements,
        effectiveMode,
        queryOptions,
        this.defaultSafeIntegerMode,
        raw,
      );

View on GitHub (pinned to bad083fafb)

Solutions

  1. Pass an array: db.batch([{ sql: 'INSERT INTO t VALUES (?)', args: [1] }], 'write')
  2. Normalize: db.batch(Array.isArray(stmts) ? stmts : [stmts])
  3. Type the variable as BatchStatement[] to catch it at compile time
  4. Await async statement builders before passing their result

Example fix

// before
await db.batch({ sql: 'INSERT INTO t VALUES (?)', args: [1] });
// TypeError: Expected first argument to be an array of statements

// after
await db.batch([{ sql: 'INSERT INTO t VALUES (?)', args: [1] }]);
Defensive patterns

Strategy: type-guard

Validate before calling

function toBatchStatements(v: BatchStatement[] | BatchStatement | undefined): BatchStatement[] {
  if (v === undefined) return [];
  return Array.isArray(v) ? v : [v];
}

await db.batch(toBatchStatements(stmts), 'write');

Type guard

function isBatchStatementArray(v: unknown): v is BatchStatement[] {
  return Array.isArray(v) && v.every((s) => typeof s === 'string' || (typeof s === 'object' && s !== null && 'sql' in s));
}

Try / catch

try {
  await db.batch(stmts);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('array of statements')) {
    throw new TypeError('db.batch expects an array — got ' + typeof stmts);
  }
  throw e;
}

Prevention

When it happens

Trigger: db.batch({ sql: 'SELECT 1' }) with a bare object; db.batch(undefined) when a statement-building function returns nothing; db.batch(promisedArray) without await; passing a SQL string directly.

Common situations: Converting sequential run() calls into a batch and forgetting the array literal; conditional statement lists that collapse to undefined; JS code without TypeScript catching the shape; spread of a non-iterable into the argument.

Related errors


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