tursodatabase/turso · error · LibsqlError
CLIENT_CLOSED
CLIENT_CLOSED
Error message
Client is closed
What it means
execute() checks the _closed flag under the client's exec lock and throws LibsqlError code CLIENT_CLOSED if the client was closed earlier. The compatibility client is not reconnectable: after close() every execute (and batch/transaction) on the same instance fails with this error.
Solutions
- Recreate the client: call createClient(config) again to get a fresh, open instance
- Check the client.closed getter before issuing queries on shared instances
- Scope close() to true app shutdown only; in serverless runtimes keep the client alive across invocations instead of closing per request
- Audit finally blocks and error handlers for premature close() calls
Example fix
// before
finally { client.close(); }
// next request:
await client.execute('SELECT 1'); // throws CLIENT_CLOSED
// after - lazy reusable client, never closed per request
let client = createClient(config);
export async function query(sql: string) {
if (client.closed) client = createClient(config);
return client.execute(sql);
} Defensive patterns
Strategy: type-guard
Validate before calling
if (client.closed) {
client = createClient(config);
}
const rs = await client.execute(sql); Type guard
function isOpen(client: Client): boolean {
return !client.closed;
} Try / catch
catch (e) { if (e instanceof LibsqlError && e.code === 'CLIENT_CLOSED') { client = createClient(config); return client.execute(sql); } throw e; } Prevention
- Close the client only at process/module teardown, never per request
- Keep serverless clients module-scoped and warm across invocations
- Audit finally blocks and error handlers for premature close() calls
When it happens
Trigger: client.execute(sql) after client.close() ran - typically a shared module-level client closed by an error handler, a shutdown hook, or hot-reload cleanup, while later requests still reference it.
Common situations: Serverless handler closes the client in a finally block but the module-level singleton is reused by the next invocation; test teardown closing the client then assertions still running queries; close-on-error followed by a retry that reuses the same instance.
Related errors
- TRANSACTION_CLOSED
- Expected first argument to be an array of statements
- UNSUPPORTED_CONFIG
- batch mode must be one of
- batch response does not have one result and one error per…
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/58d4619fd1845ce1.
Report an issue: GitHub.
Appendix: source
Thrown at serverless/javascript/src/compat.ts:307
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 6c72522679)