Profiles·Public

@libsql/client

semver>=0.3.0postconditions17functions11last verified2026-06-24coverage score85%

Postconditions: what we check

  • execute · database-error
    error
    WhenAny failure: network/DNS failure when using remote Turso URL, authentication failure (invalid/expired auth token), SQL syntax error or constraint violation (triggers LibsqlError with SQLITE_ERROR), server error (HTTP 400/500 triggers SERVER_ERROR), or database file access error.
    ThrowsLibsqlError (extends Error) — thrown on all failures. Properties: code (string error code: SERVER_ERROR, AUTH_FAILED, SQLITE_ERROR, etc.), rawCode (optional number for SQLite-specific error codes).
    Required handlingCaller MUST wrap execute() in try/catch. Database failures in Next.js Server Actions or API routes cause unhandled exceptions — no data inserted, users see generic errors. The official Next.js with-turso example omits try-catch — this is a known anti-pattern. Minimum handling: try { await db.execute({ sql: 'INSERT INTO todos (description) VALUES (?)', args: [description] }); } catch (error) { if (error instanceof LibsqlError) { console.error('Database error:', error.message, 'code:', error.code); } throw error; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1][2]
  • execute · execute-integer-range-error
    warning
    Whenexecute() is called on a table that contains SQLite INTEGER columns with values larger than 2^53-1 (Number.MAX_SAFE_INTEGER = 9007199254740991), and the createClient() was called with the default intMode: "number" (or intMode not set). The RangeError is thrown during result deserialization — after the network request succeeds — so callers who only catch LibsqlError will miss it. Common triggers: auto-increment IDs after billions of rows, Twitter/Snowflake-style IDs, UNIX timestamps in microseconds, or any INTEGER PRIMARY KEY in a high-write table.
    ThrowsRangeError (not LibsqlError) — thrown synchronously from result deserialization code in @libsql/hrana-client/lib-esm/value.js with message: "Received integer which is too large to be safely represented as a JavaScript number". This is a plain JavaScript RangeError, NOT a LibsqlError — it has no .code property. A catch block that checks `instanceof LibsqlError` will NOT catch this error.
    Required handlingOption 1 (recommended): Configure createClient() with intMode: "bigint" when tables contain large integers. BigInt can represent all SQLite INTEGER values. const db = createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN, intMode: 'bigint' // safe for all INTEGER columns }); Option 2: Catch both error types explicitly: try { const { rows } = await db.execute('SELECT id, value FROM large_table'); return rows; } catch (error) { if (error instanceof RangeError) { // Large integer in result — reconfigure client with intMode: "bigint" throw new Error('Database contains integers too large for JS number type'); } if (error instanceof LibsqlError) { console.error('Database error:', error.message, 'code:', error.code); } throw error; } Option 3: Use intMode: "string" to return all integers as strings.
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[3][1]
  • execute · execute-invalid-number-arg
    warning
    Whenexecute() is called with a parameterized query where the args array contains a JavaScript number that is NaN, Infinity, or -Infinity. This occurs when the argument is produced by a calculation that results in an invalid number (e.g., dividing by zero produces Infinity, Math.sqrt(-1) produces NaN, parseFloat of a non-numeric string produces NaN). The RangeError is thrown synchronously during argument serialization, before any network request is made. Applies to all transport modes (HTTP, WebSocket, local SQLite).
    ThrowsRangeError (not LibsqlError) — thrown synchronously from argument serialization in @libsql/client/lib-esm/sqlite3.js (local) or @libsql/hrana-client/lib-esm/value.js (remote) with message: "Only finite numbers (not Infinity or NaN) can be passed as arguments". This is a plain JavaScript RangeError, NOT a LibsqlError — it has no .code property. A catch block that only checks `instanceof LibsqlError` will NOT catch this error. Same error applies to batch() and transaction.execute() when using number args.
    Required handlingValidate computed numeric arguments before passing to execute(): const ratio = numerator / denominator; // could be Infinity or NaN // Guard before using as query argument: if (!Number.isFinite(ratio)) { throw new Error(`Invalid ratio value: ${ratio} — cannot store NaN or Infinity`); } try { await db.execute({ sql: 'INSERT INTO metrics (ratio) VALUES (?)', args: [ratio] }); } catch (error) { if (error instanceof LibsqlError) { console.error('Database error:', error.message, 'code:', error.code); } throw error; } Alternatively, catch all error types (not just LibsqlError): } catch (error) { if (error instanceof RangeError) { // Invalid numeric argument — check args for NaN/Infinity throw new Error(`Invalid argument value: ${error.message}`); } if (error instanceof LibsqlError) { ... } throw error; }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4][1]
  • execute · execute-invalid-bigint-arg
    warning
    Whenexecute() is called with a parameterized query where the args array contains a JavaScript bigint value that exceeds the SQLite INTEGER range (outside -9223372036854775808 to 9223372036854775807, i.e., -2^63 to 2^63-1). SQLite stores integers as signed 64-bit values, so bigint values outside this range cannot be represented. Common trigger: external IDs from systems using 128-bit identifiers, or bigint arithmetic that overflows the 64-bit boundary. The RangeError is thrown synchronously before any network request is made.
    ThrowsRangeError (not LibsqlError) — thrown synchronously from argument serialization in @libsql/client/lib-esm/sqlite3.js with message: "bigint is too large to be represented as a 64-bit integer and passed as argument", or in @libsql/hrana-client/lib-esm/value.js (remote transport) with message: "This bigint value is too large to be represented as a 64-bit integer and passed as argument". Note: the two messages differ slightly ("bigint is too large" vs "This bigint value is too large"). This is a plain JavaScript RangeError, NOT a LibsqlError — it has no .code property. Same error applies to batch() and transaction.execute() when using bigint args.
    Required handlingValidate bigint arguments before passing to execute(): const externalId = someExternalSystem.getId(); // could be 128-bit ID as bigint const maxSqliteInt = 9223372036854775807n; // 2^63 - 1 const minSqliteInt = -9223372036854775808n; // -(2^63) if (externalId > maxSqliteInt || externalId < minSqliteInt) { throw new Error(`ID ${externalId} exceeds SQLite INTEGER range — store as TEXT instead`); } try { await db.execute({ sql: 'INSERT INTO items (external_id) VALUES (?)', args: [externalId] }); } catch (error) { if (error instanceof RangeError) { throw new Error(`Argument bigint out of range: ${error.message}`); } if (error instanceof LibsqlError) { ... } throw error; } Note: For IDs that may exceed 64-bit range, use TEXT column type and pass the bigint as a string: args: [externalId.toString()].
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4][1]
  • batch · database-error
    error
    WhenAny failure in any statement in the batch: network failure, auth failure, SQL error, constraint violation, or server error.
    ThrowsLibsqlError — on any failure in the batch. The entire batch is atomic on failure.
    Required handlingCaller MUST wrap batch() in try/catch. try { await db.batch([ 'INSERT INTO users (name) VALUES ("Alice")', { sql: 'INSERT INTO profiles (user_id) VALUES (?)', args: [newUserId] } ], 'write'); } catch (error) { console.error('Batch failed:', error); throw error; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[1][2]
  • transaction · transaction-not-closed
    error
    Whentransaction() is called and the returned Transaction object is not closed in a finally block. If execute() or commit() throws, the transaction remains open and holds a write lock indefinitely (or until the 5-second server timeout expires).
    ThrowsDoes not throw at the call site, but open transactions cause subsequent write operations to queue or fail with SQLITE_BUSY while the lock is held. After 5-second server timeout, the connection is forcibly closed.
    Required handlingCaller MUST always call transaction.close() in a finally block. This is the canonical pattern from official docs: const tx = await client.transaction("write"); try { await tx.execute({ sql: 'INSERT INTO orders ...', args: [...] }); await tx.execute({ sql: 'UPDATE inventory ...', args: [...] }); await tx.commit(); } catch (error) { // commit failed or execute failed — log and rethrow console.error('Transaction failed:', error); throw error; } finally { tx.close(); // always close — safe to call if already committed } Omitting finally causes write-lock starvation in high-traffic apps.
    costhighin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[5][1]
  • transaction · transaction-open-error
    error
    Whentransaction() is awaited without try-catch. If the database connection fails during the initial BEGIN IMMEDIATE, the promise rejects with a LibsqlError and the transaction object is never returned.
    ThrowsLibsqlError with code SERVER_ERROR (HTTP transport failure), HRANA_WEBSOCKET_ERROR (WebSocket transport failure), CLIENT_CLOSED (if client was already closed), or SQLITE_ERROR (if BEGIN fails due to schema lock). Confirmed from hrana.js source: mapHranaError() wraps all Hrana errors to LibsqlError.
    Required handlingWrap await client.transaction() in a try/catch: try { const tx = await client.transaction("write"); // ... use tx ... } catch (error) { if (error instanceof LibsqlError && error.code === 'CLIENT_CLOSED') { // client was closed before transaction opened — reconnect await client.reconnect(); } throw error; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6]
  • transaction.commit · commit-no-try-catch
    error
    Whentransaction.commit() is called without try-catch. If the commit fails due to network error, server restart, or connection drop, the promise rejects and all transaction changes are lost silently if the error is not caught and surfaced to the user.
    ThrowsLibsqlError with code SERVER_ERROR (network failure during commit), HRANA_WEBSOCKET_ERROR (WebSocket closed during commit), TRANSACTION_CLOSED (if transaction was already closed — code "TRANSACTION_CLOSED" with message "Cannot commit the transaction because it is already closed"). Confirmed from hrana.js: 'throw new LibsqlError("Cannot commit the transaction because it is already closed", "TRANSACTION_CLOSED")'.
    Required handlingCaller MUST wrap transaction.commit() in a try/catch. A failed commit means data was NOT persisted even though the application logic ran. This is a critical distinction — the application may believe data was saved when it was not. const tx = await client.transaction("write"); try { await tx.execute({ sql: '...', args: [...] }); await tx.commit(); // MUST be in try block } catch (error) { // commit failure = data was NOT saved console.error('Commit failed — data not persisted:', error); throw error; } finally { tx.close(); }
    costcriticalin prodsilent failureusers seelost datavisibilitysilent
    Sources[6]
  • transaction.execute · transaction-execute-no-try-catch
    error
    Whentransaction.execute() is called without try-catch inside a transaction block. If the statement fails with a SQL error or constraint violation, the error propagates and the transaction may be left open (no automatic rollback on execute failure — caller must explicitly rollback or close).
    ThrowsLibsqlError with code SQLITE_ERROR (SQL syntax, constraint violation e.g. UNIQUE constraint failed), TRANSACTION_CLOSED (if transaction was closed before this call — e.g., timed out by server), SERVER_ERROR (network failure). extendedCode contains SQLite extended error code (e.g., SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_UNIQUE).
    Required handlingCaller MUST wrap each transaction.execute() in try-catch and explicitly rollback or let the finally block's close() handle cleanup: const tx = await client.transaction("write"); try { await tx.execute({ sql: 'INSERT ...', args: [...] }); // throws if UNIQUE violated await tx.commit(); } catch (error) { // transaction not auto-rolled back — close() will roll it back throw error; } finally { tx.close(); // rolls back if not committed } Note: after execute() throws, the transaction is NOT automatically rolled back. Calling tx.close() in finally handles rollback automatically.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6]
  • migrate · migrate-no-try-catch
    error
    Whenclient.migrate() is called without try-catch in a migration script or startup hook. If a migration statement fails (syntax error, column exists, table already exists), the Promise rejects and remaining statements are not executed, but any previously executed statements in OTHER calls are already committed (migrate is not atomic across multiple migrate() calls).
    ThrowsLibsqlError with code SQLITE_ERROR (most common: table already exists, column already exists, syntax error), SERVER_ERROR (network failure), HRANA_PROTO_ERROR (protocol error). Wraps LibsqlBatchError on individual statement failures with statementIndex indicating which statement failed.
    Required handlingCaller MUST wrap migrate() in try/catch to detect failed migrations. In startup initialization, unhandled migration errors crash the process silently in some frameworks (e.g., Next.js swallows module-load errors). try { await db.migrate([ 'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)', 'CREATE INDEX IF NOT EXISTS users_email ON users (email)', ]); } catch (error) { console.error('Migration failed:', error.message); if (error instanceof LibsqlBatchError) { console.error('Failed at statement index:', error.statementIndex); } process.exit(1); // abort startup on migration failure }
    costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7][2]
  • executeMultiple · execute-multiple-no-try-catch
    warning
    Whenclient.executeMultiple() is called without try-catch. If any statement in the SQL string fails, the error is thrown and remaining statements are not executed. Because there is no wrapping transaction, statements that ran before the failure are permanently committed — creating partial execution.
    ThrowsLibsqlError with code SQLITE_ERROR (syntax error, constraint violation, unknown table), SERVER_ERROR (network failure), or HRANA_PROTO_ERROR. Unlike batch(), does NOT throw LibsqlBatchError — there is no statement index in the error.
    Required handlingCaller MUST wrap executeMultiple() in try-catch to detect partial execution: try { await db.executeMultiple(` CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT); CREATE INDEX IF NOT EXISTS users_email ON users (email); INSERT INTO schema_version (version) VALUES (2); `); } catch (error) { console.error('executeMultiple failed:', error.message); // WARNING: statements before the failure are permanently committed! // You may need to handle partial state manually. throw error; } Prefer batch() for programmatic multi-statement operations where you need atomicity. Use executeMultiple() only for SQL scripts where partial execution risk is understood.
    costhighin proddegraded serviceusers seelost datavisibilitysilent
    Sources[6][2]
  • transaction.rollback · rollback-already-closed
    warning
    Whentransaction.rollback() is called after the transaction is already closed (e.g., after close() was already called, or after a server-side timeout forcibly closed the connection). Rollback throws rather than silently succeeding.
    ThrowsLibsqlError with code HRANA_CLOSED_ERROR (if underlying stream was closed by network failure) or SERVER_ERROR (if server rejected the ROLLBACK). Confirmed from mapHranaError() in hrana.js: ClosedError → "HRANA_CLOSED_ERROR".
    Required handlingIn explicit error handling flows, wrap rollback() in its own try-catch to avoid masking the original error: const tx = await client.transaction("write"); try { await tx.execute({ sql: '...', args: [...] }); await tx.commit(); } catch (error) { // Don't let rollback error mask the original error try { await tx.rollback(); } catch (rollbackErr) { console.error('Rollback also failed (tx may have already closed):', rollbackErr); } throw error; // rethrow original error } finally { tx.close(); } In most cases, using tx.close() in a finally block is simpler and equivalent — close() calls rollback internally without throwing.
    costlowin prodimmediate exceptionusers seedegraded performancevisibilityvisible
    Sources[6]
  • sync · sync-wrong-client-type
    error
    Whensync() is called on a client configured with an http:// or wss:// URL (standard Turso cloud connection) rather than a syncUrl-based embedded replica configuration. This is the most common sync() mistake — callers expect sync() to sync their cloud data when it actually only works for local SQLite embedded replicas.
    ThrowsLibsqlError with code SYNC_NOT_SUPPORTED. Message: "sync not supported in http mode" or "sync not supported in ws mode". Throws synchronously before any network call.
    Required handlingOnly call sync() when createClient() was configured with syncUrl and url pointing to a local file path. Always check your client configuration before calling sync(), or wrap in try-catch to handle SYNC_NOT_SUPPORTED gracefully: try { await client.sync(); } catch (error) { if (error instanceof LibsqlError && error.code === 'SYNC_NOT_SUPPORTED') { // Client is not an embedded replica — skip sync return; } throw error; }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[7][8][2]
  • sync · sync-network-failure
    warning
    Whensync() is called on a correctly configured embedded replica client, but the network connection to the primary remote database fails (timeout, unreachable host, auth expiry). The native libsql-darwin/linux/windows addon propagates the failure as a LibsqlError.
    ThrowsLibsqlError with code SERVER_ERROR, AUTH_FAILED, or HRANA_WEBSOCKET_ERROR depending on failure mode. The exact code comes from the native addon's error translation layer.
    Required handlingWrap sync() calls in try-catch, especially in background sync jobs or application startup paths. An offline embedded replica that fails to sync continues serving reads from its local copy — callers should log sync failures and continue rather than crashing: try { const replicated = await client.sync(); console.log(`Synced: ${replicated?.frames_synced} frames`); } catch (error) { console.error('Sync failed — serving stale replica data:', error); // Do NOT throw — local reads still work }
    costmediumin proddegraded serviceusers seedegraded performancevisibilitysilent
    Sources[4][2]
  • sync · sync-client-closed
    error
    Whensync() is called on a closed embedded replica client (after close() was called or the client encountered an unrecoverable error). The #checkNotClosed() guard fires before any sync attempt.
    ThrowsLibsqlError with code CLIENT_CLOSED. Message: "The client is closed".
    Required handlingDo not call sync() on closed clients. In background sync loops, check client.closed before calling sync(): if (!client.closed) { await client.sync(); }
    costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[4]
  • transaction.batch · transaction-batch-no-try-catch
    error
    Whentransaction.batch() is called without try-catch inside a transaction block. If any statement fails (SQL syntax, constraint violation, network failure) or the transaction stream was already closed, the promise rejects. Unlike client.batch() (which atomically rolls back its implicit transaction), tx.batch() leaves the surrounding interactive transaction OPEN on failure — the caller must explicitly rollback or rely on tx.close() in finally. A bare await without try-catch propagates the error up the call stack, potentially leaving the write-lock held until server timeout (5s).
    ThrowsLibsqlError with code TRANSACTION_CLOSED if the transaction was already closed before this call (e.g., timed out by server or after a prior commit). Message: "Cannot execute statements because the transaction is closed". LibsqlBatchError with statementIndex if a specific statement fails inside the batch (extends LibsqlError, code SQLITE_ERROR or extended code such as SQLITE_CONSTRAINT_UNIQUE). SERVER_ERROR / HRANA_WEBSOCKET_ERROR on transport failure. Confirmed from hrana.js: tx.batch throws TRANSACTION_CLOSED then mapHranaError() on any inner failure.
    Required handlingAlways wrap transaction.batch() in try-catch with tx.close() in finally: const tx = await client.transaction("write"); try { await tx.batch([ 'INSERT INTO orders (user_id) VALUES (1)', { sql: 'UPDATE inventory SET qty = qty - 1 WHERE id = ?', args: [42] }, ]); await tx.commit(); } catch (error) { if (error instanceof LibsqlBatchError) { console.error('Batch failed at statement', error.statementIndex); } // transaction NOT auto-rolled back — close() in finally handles cleanup throw error; } finally { tx.close(); }
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible
    Sources[6][2]
  • transaction.executeMultiple · transaction-execute-multiple-no-try-catch
    warning
    Whentransaction.executeMultiple() is called without try-catch inside a transaction block. If any statement in the SQL string fails or the transaction stream was closed beforehand, the promise rejects. Statements that ran before the failing one are part of the interactive transaction — if commit() is not subsequently called (because an exception propagated), all of them are rolled back when tx.close() runs. But the caller must actually call tx.close() — a bare await without try-catch-finally leaks the transaction and holds the write lock for up to 5 seconds.
    ThrowsLibsqlError with code TRANSACTION_CLOSED if the transaction stream was closed before this call. Message: "Cannot execute statements because the transaction is closed". LibsqlError with code SQLITE_ERROR (syntax error, constraint violation, unknown table) or SERVER_ERROR (network failure) on inner statement failure. Confirmed from hrana.js executeMultiple() catch block — the transaction is closed automatically on inner failure (this.close() is called inside catch).
    Required handlingAlways wrap transaction.executeMultiple() in try-catch with tx.close() in finally. The auto-close-on-failure behavior of executeMultiple is a one-shot — subsequent tx.execute calls will throw TRANSACTION_CLOSED: const tx = await client.transaction("write"); try { await tx.executeMultiple(` INSERT INTO orders (user_id) VALUES (1); UPDATE inventory SET qty = qty - 1 WHERE id = 42; `); await tx.commit(); } catch (error) { console.error('Multi-statement failed:', error); // transaction may already be closed by executeMultiple's internal catch throw error; } finally { tx.close(); // safe to call even if already closed }
    costhighin proddegraded serviceusers seelost datavisibilitysilent
    Sources[6][2]

Sources

Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.

Official documentation
Source code

Research notes

Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.

Sources — @libsql/client

Claim: execute() throws LibsqlError

Source: GitHub repository — tursodatabase/libsql-client-ts https://github.com/tursodatabase/libsql-client-ts

LibsqlError is the error class thrown on all failures. Properties: code (string), rawCode (optional number).

Source: GitHub issues showing LibsqlError in practice: https://github.com/tursodatabase/libsql-client-ts/issues/202 "LibsqlError: SERVER_ERROR: Server returned HTTP status 400"

Claim: Official examples omit try-catch

Source: Official Next.js with-turso example https://github.com/vercel/next.js/tree/canary/examples/with-turso

Both addTodo and removeTodo server actions call db.execute() without try-catch.

Claim: Error codes

Source: GitHub issues and community discussions

  • SERVER_ERROR: HTTP errors from Turso API
  • AUTH_FAILED: Invalid auth token
  • URL_INVALID: Bad database URL
  • SQLITE_ERROR: SQLite constraint/syntax errors

Package Metadata

Need a different package?
Request a profile