Errors & SQLSTATEs
Connection and transient
| SQLSTATE | Meaning | Retry? |
|---|---|---|
08006 | The connection dropped, or an object-storage hop failed while the statement was in flight. | Yes. Reconnect, then re-run. If the statement was a COMMIT, check whether it landed before replaying. |
XX000 | Internal error. The message is preserved verbatim. | Once. A repeat is a bug: report the message. |
57014 | canceling statement due to statement timeout — your statement_timeout expired. | Your call. The statement was cancelled, not applied. |
53400 | A resource limit on the data plane was reached (for example the per-connection concurrency limit). | Yes, with backoff. |
Transactions
| SQLSTATE | Meaning | Retry? |
|---|---|---|
40001 | Serialization failure at COMMIT: a key this transaction wrote, or read under FOR UPDATE, was committed by another transaction first. | Yes. Re-run the entire transaction from BEGIN and recompute every value. See Transactions. |
Data
| SQLSTATE | Meaning | Example |
|---|---|---|
22001 since 0.1.43 | A string is longer than its declared VARCHAR(n) / CHAR(n). Nothing is stored, nothing is truncated. | value too long for type character varying(3) — column "code" |
22003 | A number is outside its column’s range (SMALLINT, 32-bit int, REAL overflow to infinity), a NUMERIC(p,s) exceeds its precision, or a value for a bare NUMERIC has more than 9 fractional digits. Nothing is wrapped or rounded away silently. | value 40000 is out of range for column "s" (SMALLINT) · value for column "amount" is out of range for DECIMAL(18,2) · … out of range for NUMERIC(38,9): the scale was chosen by the engine (bare NUMERIC), and storing this value would round away digits past 9 fractional places |
22012 since 0.1.43 | Division by zero in a NUMERIC expression. | division by zero |
22P02 | A value cannot be read as its column’s type: garbage in a BOOLEAN, DATE or TIMESTAMP, an impossible date such as 2026-13-45, a non-UUID string in a UUID column. Never stored as NULL. | invalid input syntax for column "ts" (TIMESTAMP): "not-a-time" · invalid input syntax for column "u" (UUID): not-a-uuid |
None of these is retryable.
Integrity
| SQLSTATE | Meaning | Example |
|---|---|---|
23505 | Primary key or UNIQUE INDEX violated. A plain INSERT of an existing key is this, by design. | duplicate key value violates unique constraint "orders_pkey": Key (id)=(1) already exists. |
23502 | NOT NULL violated, or a required column missing from the row. | null value in column "user_id" violates not-null constraint |
23514 | A CHECK constraint rejected the row. | new row for relation "accounts" violates check constraint "accounts_balance_check" |
23P90 | The write touches a row sealed by an IMMUTABLE rule (a closed accounting period, for instance). | … sealed by immutable rule "period_2026_08" |
23503 | An INSERT or UPSERT names a parent row that does not exist. Checked on the insert side only; deleting a parent with children succeeds. | insert or update on table "child" violates foreign key constraint "child_pid_fkey": Key (id)=(999) is not present in table "parent". |
Fix the data, not the retry loop. ORMs map these to their IntegrityError class.
Schema and SQL
| SQLSTATE | Meaning | Example |
|---|---|---|
42601 | Syntax error, or a statement outside the SQL surface: UPDATE/DELETE without WHERE, CREATE VIEW, CREATE SEQUENCE, LOCK TABLE, DROP INDEX, COPY … TO, DROP TABLE a, b, a function call inside VALUES. The message names the statement and the alternative. | statement is not part of the tables SQL surface: CREATE VIEW … |
42703 | Undefined column; also the unsupported ALTER TABLE forms (RENAME, SET DEFAULT, …) and a literal DEFAULT whose type does not match a NUMERIC column. | column "nope" not found in table "t" · ALTER TABLE form not supported … · DEFAULT literal 0 does not match column type decimal(18,2) |
42P01 | Undefined table; also pg_catalog objects and functions that are not emulated (pg_advisory_lock(), information_schema.table_constraints). | table "orders" does not exist |
42P07 | CREATE TABLE of a name that already exists (without IF NOT EXISTS). | table "orders" already exists |
42830 | A FOREIGN KEY references columns that are not the parent’s declared primary key. | there is no unique constraint matching given keys for referenced table |
42P01 (in DDL) | A FOREIGN KEY names a parent table that does not exist. Create the parent first. | table "parent" does not exist |
Feature not supported
0A000 is reserved for features the engine recognises and has deliberately not implemented; the message always says what to do instead. Every other refused shape comes back as 42601 or 42703 with the alternative named in the message (see above). The 0A000 cases:
triggers are not supported yet: a synchronous plpgsql-subset trigger engine … is on the roadmap. Today, react to writes with an ingest rule → pipeline, or an Action; validation belongs in CHECK constraints.since 0.1.43cannot use subquery in check constraint, and aCHECKwith arithmetic or function calls — the message lists the supported predicate forms.- A
FOREIGN KEYdeclaredDEFERRABLE,INITIALLY DEFERRED, or withON DELETE/ON UPDATECASCADE/SET NULL/SET DEFAULT. The engine enforces only what its write gate can keep, so these options are refused at DDL rather than accepted and ignored. Drop the option; cascade in the application.
The full current list is on Limits & differences.
Authentication
| SQLSTATE | Meaning |
|---|---|
28P01 | FATAL: authentication failed at connect. Wrong API-key secret, service-account secret or expired JWT. The secret is never echoed. |
28000 | The credential is valid but may not use this database (bucket), or the session context was rejected. |
Building a retry loop
RETRY_TXN = {"40001"} # re-run the whole transaction
RETRY_CONN = {"08006"} # reconnect first
RETRY_ONCE = {"XX000"}
def should_retry(sqlstate, attempt):
if sqlstate in RETRY_TXN or sqlstate in RETRY_CONN:
return attempt < 8
if sqlstate in RETRY_ONCE:
return attempt < 1
return False # 22xxx, 23xxx, 42xxx, 0A000, 28xxx: neverTwo rules make the loop safe. First, retry the transaction, not the statement: a 40001 means the values you computed are stale. Second, make the transaction idempotent before you retry a connection failure, because the previous attempt may have committed; the transfer pattern shows the journal-row technique.
NOTICEs you will see
A NOTICE is not an error. It arrives on the same connection and most drivers hand it to a callback rather than raising.
| When | Notice says | Meaning |
|---|---|---|
Bare NUMERIC / DECIMAL in CREATE TABLE | the column defaults to NUMERIC(38,9); declare NUMERIC(p,s) | Declare the precision and scale you mean, e.g. NUMERIC(18,2) for money, NUMERIC(38,0) for integer minor units. |
FOREIGN KEY on a cluster whose operator has turned FK enforcement off | constraint accepted for compatibility but NOT ENFORCED by the tables engine | Not the default. If you see it, referential integrity is the application’s job on that cluster. |
SELECT … FOR UPDATE outside a transaction | row-locking clause outside a transaction has no effect: served as a snapshot read | Wrap it in BEGIN … COMMIT if you meant to guard it. |
TIMESTAMP WITHOUT TIME ZONE in DDL | TIMESTAMP WITHOUT TIME ZONE is stored with timestamptz semantics … | The column is stored as UTC microseconds. |
SET TimeZone = … | TimeZone has no effect on Dodil Tables: timestamps are stored and rendered in UTC … | The statement succeeded and changed nothing. |
ALTER TABLE … ADD CONSTRAINT … UNIQUE | accepted, not enforced | Use CREATE UNIQUE INDEX for the enforced form. |
VARCHAR(n) and TIMESTAMP(p) no longer raise a precision notice: both are honoured since 0.1.43.
Reading them:
# psycopg 3
conn.add_notice_handler(lambda d: print(d.severity, d.message_primary))// node-postgres
client.on('notice', (msg) => console.warn(msg.message))A strong read that would be incomplete
Under a sustained write burst a read can be refused rather than served partially:
strong read aborted: the WAL overlay truncated at N entries, so the result
would be incomplete — compaction is catching up; retry shortlyOn the Postgres wire this arrives as XX000 with that message; on gRPC it is FAILED_PRECONDITION. The engine never returns a partial answer in its place. Compaction is already scheduled when this fires, so a retry after a short pause converges. If you see it often, run dodil data table compact after your bulk loads; see Performance & layout.