tursodatabase/turso · error · LibsqlError

CLIENT_CLOSED

CLIENT_CLOSED

Error message

Client is closed

What it means

execute() on the compatibility-layer client acquires the exec lock and then checks the _closed flag, throwing a LibsqlError with code CLIENT_CLOSED if the client was closed. close() is synchronous: it flips _closed immediately and closes the underlying session fire-and-forget, so any execute() issued after close() rejects with this error rather than reaching the server.

Source

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

    if (options != null && typeof options === "object") {
      return {
        mode: options.mode,
        raw: options.raw === true,
      };
    }
    return {
      mode: options,
      raw: false,
    };
  }

  async execute(stmt: InStatement): Promise<ResultSet>;
  async execute(sql: string, args?: InArgs): Promise<ResultSet>;
  async execute(stmtOrSql: InStatement | string, args?: InArgs): Promise<ResultSet> {
    await this.execLock.acquire();
    try {
      if (this._closed) {
        throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
      }

      let normalizedStmt: { sql: string; args: any[] };

      if (typeof stmtOrSql === 'string') {
        const normalizedArgs = args ? (Array.isArray(args) ? args : Object.values(args)) : [];
        normalizedStmt = { sql: stmtOrSql, args: normalizedArgs };
      } else {
        normalizedStmt = this.normalizeStatement(stmtOrSql);
      }

      const result = await this.session.execute(normalizedStmt.sql, normalizedStmt.args, this._defaultSafeIntegers);
      return this.convertResult(result);
    } catch (error: any) {
      if (error instanceof LibsqlError) {
        throw error;
      }
      throw mapDatabaseError(error, "EXECUTE_ERROR");

View on GitHub (pinned to bad083fafb)

Solutions

  1. Do not reuse the client after close() — create a fresh one with createClient(config) for the next unit of work
  2. Check the public client.closed getter before issuing queries and recreate the client when it is true
  3. Move close() out of per-request paths; only close at process exit or when the owner that created the client is done
  4. Fix ordering bugs where close() runs before a pending execute() promise settles

Example fix

// before
client.close();
const rs = await client.execute('SELECT 1'); // LibsqlError CLIENT_CLOSED

// after
if (client.closed) {
  client = createClient(config);
}
const rs = await client.execute('SELECT 1');
Defensive patterns

Strategy: validation

Validate before calling

async function executeOrRecreate(sql: string) {
  if (client.closed) {
    client = createClient(config);
  }
  return client.execute(sql);
}

Type guard

const isUsable = (c: Client): boolean => !c.closed;

Try / catch

try {
  const rs = await client.execute(sql);
} catch (e) {
  if (e instanceof LibsqlError && e.code === 'CLIENT_CLOSED') {
    client = createClient(config);
    return client.execute(sql);
  }
  throw e;
}

Prevention

When it happens

Trigger: Sequencing like client.close(); await client.execute(sql). A shared module-level client that one request or test closes (e.g. in afterEach) while another concurrent request still calls execute(). A close in a finally block followed by a retry of the same query.

Common situations: Test suites that close a singleton client in teardown while later tests reuse it; serverless warm invocations reusing a client that a previous invocation closed during cleanup; race conditions where cleanup code closes the client before an in-flight promise calls execute.

Related errors


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