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

batch() on the compatibility-layer client validates that its first argument is an array and throws a plain TypeError otherwise. This runs after the closed-client check but before any options normalization, so it fires synchronously (inside the async method) regardless of database state. It mirrors better-sqlite3/libsql-style batch APIs, which only accept arrays of statements.

Source

Thrown at serverless/javascript/src/compat.ts:330

    } catch (error: any) {
      if (error instanceof LibsqlError) {
        throw error;
      }
      throw mapDatabaseError(error, "EXECUTE_ERROR");
    } finally {
      this.execLock.release();
    }
  }

  async batch(stmts: Array<InStatement>, options?: TransactionMode | BatchOptions): Promise<Array<BatchResultSet>> {
    await this.execLock.acquire();
    try {
      if (this._closed) {
        throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
      }

      if (!Array.isArray(stmts)) {
        throw new TypeError("Expected first argument to be an array of statements");
      }

      const { mode, raw } = this.normalizeBatchOptions(options);
      const batchMode = mode ?? "deferred";

      const results = await this.session.batch(
        stmts,
        batchMode,
        undefined,
        this._defaultSafeIntegers,
        raw,
      );

      return results.map((result: any) => this.convertBatchResult(result));
    } catch (error: any) {
      if (error instanceof LibsqlError) {
        throw error;
      }

View on GitHub (pinned to bad083fafb)

Solutions

  1. Wrap the statement(s) in an array: client.batch([{ sql: 'SELECT 1' }])
  2. If a variable list may be a single statement, normalize first: const stmts = Array.isArray(x) ? x : [x]
  3. Add explicit TypeScript types (InStatement[]) at the call site so the compiler catches this before runtime
  4. Check for a missing await when the statements come from an async builder

Example fix

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

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

Strategy: type-guard

Validate before calling

function toStmtArray(stmts: InStatement | InStatement[] | undefined): InStatement[] {
  if (stmts === undefined) return [];
  return Array.isArray(stmts) ? stmts : [stmts];
}

await client.batch(toStmtArray(input));

Type guard

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

Try / catch

try {
  await client.batch(stmts);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('array of statements')) {
    throw new TypeError('client.batch expects an array — wrap single statements in [ ]');
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a single statement object instead of an array: client.batch({ sql: 'SELECT 1' }). Passing undefined, a string, a generator, or a promise-of-array (forgetting await on a function that builds the array). Passing a Map or other array-like that fails Array.isArray.

Common situations: Refactoring execute(stmt) calls to batch() and forgetting to wrap the single statement in brackets; dynamically built statement lists where an empty branch yields undefined; TypeScript types bypassed with any so the compiler never flags it.

Related errors


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