Errors
One normalized DbError with codes, SQLSTATE passthrough, retryable, and the indeterminate flag for writes with unknown outcomes.
Every error that escapes the client is a DbError (or carries one as its
cause), regardless of which adapter or transport produced it. Driver errors
keep their information: the original error is attached as cause, and the
PostgreSQL SQLSTATE code is passed through when the server reported one.
The shape
import { isDbError } from "dbsdk";
try {
await db.sql`select * from does_not_exist`;
} catch (error) {
if (isDbError(error)) {
error.code; // "SYNTAX" here (SQLSTATE 42P01)
error.sqlstate; // "42P01"
error.retryable; // false
error.indeterminate; // false: nothing was written
error.adapterId; // "neon" | "supabase" | "postgres"
error.cause; // the underlying driver error, preserved
}
}
Codes
| Code | Meaning | Typical source |
|---|---|---|
CAPABILITY |
The transport cannot do this operation | transaction() on Neon HTTP, session-state statements on the transaction pooler |
CONFIGURATION |
The adapter was configured inconsistently | connection string contradicting the declared Supabase mode; undefined parameters |
CONSTRAINT |
Constraint violation | SQLSTATE class 23, for example unique violations (23505) |
CONNECTION |
The connection failed or was closed | dropped socket, operations after close(), Node transport errors like EPIPE or ECONNRESET |
TIMEOUT |
A timeout fired | SQLSTATE 57014, client statement timeouts |
PERMISSION |
The server refused for permissions or row security | SQLSTATE 42501, the common RLS denial |
SYNTAX |
The server could not parse the SQL | SQLSTATE class 42 except permission errors |
TRANSACTION |
Transaction-state problems | serialization failures (40001), deadlocks (40P01), invalid transaction state |
DATA |
Data exceptions | SQLSTATE class 22, for example invalid text representation |
UNKNOWN |
Anything not yet classified | driver-specific failures without an SQLSTATE |
Classification runs on the SQLSTATE when there is one, so PostgreSQL’s own
taxonomy is preserved. Code branching plus sqlstate gives you both a
stable vocabulary and exact precision when you need it.
retryable
true on connection failures, timeouts, serialization failures (40001), and
deadlocks (40P01). It means “retrying this exact statement is safe in
principle”. It is information only: dbSDK never retries on your behalf.
indeterminate
true when a write may or may not have been committed and dbSDK cannot
know which. Set when a connection or timeout error occurs:
- during a statement whose first keyword is a write (
insert,update,delete,merge,create,grant, and similar - note that allexplainstatements are included, becauseexplain analyzeexecutes its statement), - inside a batch that contains any write-shaped statement,
- escaping a
transaction()callback, where dbSDK cannot know whether your callback wrote before the failure, - in a multi-statement parameterless string, where everything after a semicolon also ran.
Top-level read-only statements are never marked on a transport failure. Inside a transaction, any error without a server SQLSTATE is marked, because dbSDK cannot know whether the callback wrote. Pre-dispatch failures (capability, configuration) are never marked, because nothing was sent.
What dbSDK does about it: nothing automatic. No retry, no replay, no failover. See Transactions for how to make retries safe with idempotent writes.
Errors that are not from the database
Your own exceptions are normalized too. If ordinary code inside
transaction() throws (a business-rule error, for example), what reaches
your catch is a DbError with code UNKNOWN, indeterminate: true (the
transaction was rolled back, but dbSDK cannot prove nothing was written
before the throw), your message, and the original error on cause. If you
want clean handling of deliberate aborts, throw a DbError yourself:
user-thrown DbErrors pass through unchanged, keeping their code and
indeterminate: false.
That also matters for the retry pattern above: indeterminate: true on a
transaction can be a business abort, not a network problem. Distinguish by
error.code === 'UNKNOWN' and the cause chain, or by throwing typed
errors for expected cases.
Reporting and diagnostics
DbError messages contain no credentials. Connection strings stay in your
environment; configuration errors name only the host, port, and username
pieces needed to diagnose a mode mismatch. The cause chain is preserved
for logs, so log the full error, not just the message.