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
- Do not reuse the client after close() — create a fresh one with createClient(config) for the next unit of work
- Check the public client.closed getter before issuing queries and recreate the client when it is true
- Move close() out of per-request paths; only close at process exit or when the owner that created the client is done
- 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
- Check the public client.closed getter before issuing queries on shared clients
- Close clients only in the scope that created them, at true end-of-life (process exit)
- Await all in-flight work before calling close() to avoid racing a settling execute()
- In tests, create a fresh client per suite instead of closing a global singleton in afterEach
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
- Database is closed
- TRANSACTION_CLOSED
- The database connection is not open
- Cannot operate on a closed cursor
- Cannot operate on a closed connection
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/58d4619fd1845ce1.
Report an issue: GitHub.