Now-Next

SaaS architecture

PostgreSQL error codes (SQLSTATE), with measured causes

Over thirty PostgreSQL errors by SQLSTATE — 23505, 42501, 42P01, 42703, 25P02, 40001 and more — each with the cause we reproduced and the measurement.

Chris van Eijk, Now-Next · · 8 min read

Every PostgreSQL error carries a five-character SQLSTATE: 23505 is a duplicate key, 42501 a missing privilege, 42P01 a table that cannot be found. The code is what your driver exposes (e.pgcode in psycopg2, e.sqlstate in psycopg 3, SQLState in JDBC) and what a retry loop should look at, because the message text changes between versions and the code does not. The first two characters are the class: 23 integrity, 25 transaction state, 40 “roll back and retry”, 42 syntax or access.

Below are the errors we reproduced for the articles on this site — on PostgreSQL 17, most also on 16 and 18 — by code, with the first cause to check and the article where each is measured. The articles reproduce causes; none of them counts how often each occurs in the wild. The condition names are from PostgreSQL’s own list of error codes.

Which SQLSTATE means what?

SQLSTATECondition nameMessageFirst cause to checkArticle
0A000feature_not_supportedcannot alter type of a column used in a policy definition (or … by a view or rule)widening a column that a policy, view or generated column usesvalue too long
0A000feature_not_supportedCOPY FROM not supported with row-level securityrestoring one tenant’s data as a role subject to row-level securitypg_dump one tenant
21000cardinality_violationmore than one row returned by a subquery used as an expressiona lookup that stopped being unique, often a key that is unique per tenant queried without the tenantmore than one row returned by a subquery
22001string_data_right_truncationvalue too long for type character varying(n)a value longer than the limit; the message names no columnvalue too long
22021character_not_in_repertoireinvalid byte sequence for encoding “UTF8”: 0x…a NUL byte, or a Windows-1252 or Latin-1 file loaded as UTF-8invalid byte sequence
22P02invalid_text_representationinvalid input syntax for type uuid: ""an empty string, or a missing value turned into text ('undefined', 'null'), cast to uuid — a real NULL gives no errorinvalid input syntax for type uuid
23502not_null_violationnull value in column “id” … violates not-null constraintthe id column lost its default, or the insert sends NULLnull value in column “id”
23503foreign_key_violationinsert or update on table … violates foreign key constraint (or update or delete on table … , still referenced)a child inserted before its parent, or a parent deleted while children point at it; on PostgreSQL 18 an ON DELETE RESTRICT key raises 23001 insteadviolates foreign key constraint
23505unique_violationduplicate key value violates unique constraint ”…_pkey”a sequence behind the table after an importduplicate key value
25P01no_active_sql_transactionSET LOCAL can only be used in transaction blocks (a warning)SET LOCAL in autocommit, so the setting never applies; never raised as an exception, only in the notices or the logPgBouncer and row-level security
25P02in_failed_sql_transactioncurrent transaction is aborted, commands ignored until end of transaction blockan earlier error in the same transaction was caught and ignoredcurrent transaction is aborted
25P03idle_in_transaction_session_timeoutterminating connection due to idle-in-transaction timeouta transaction left open while the application did something elseidle in transaction
25P04transaction_timeoutterminating connection due to transaction timeouttransaction_timeout (PostgreSQL 17) reached, also by one long statementidle in transaction
25006read_only_sql_transactioncannot execute … in a read-only transactiona write in a session or role set to default_transaction_read_onlyread-only user
28000invalid_authorization_specificationrole ”…” does not exist (as FATAL, at login)no -U, so psql used the operating-system user (root in a container), or POSTGRES_USER changed on an existing volume; over a password connection it shows as password authentication failedrole does not exist
28P01invalid_passwordpassword authentication failed for user ”…”a wrong password, but also a role that does not exist or has no (valid SCRAM) password — only the server log tells which; on Docker, a POSTGRES_PASSWORD changed on an existing volumepassword authentication failed
2BP01dependent_objects_still_existcannot drop table … because other objects depend on ita view, foreign key, function or policy on another object that refers to itcannot drop … depend on it
40001serialization_failurecould not serialize access due to concurrent update (or … read/write dependencies, under SERIALIZABLE)REPEATABLE READ or SERIALIZABLE and a row changed by another transactioncould not serialize access
40001serialization_failurecanceling statement due to conflict with recoverya long query on a read replica while the primary cleaned up rowsconflict with recovery
40P01deadlock_detecteddeadlock detectedtwo transactions locking the same rows in a different orderdeadlock detected
42501insufficient_privilegepermission denied for table …a table created after the GRANT … ON ALL TABLES, or default privileges set for the wrong rolepermission denied for table
42501insufficient_privilegepermission denied for schema publicPostgreSQL 15 and later: no CREATE on public for ordinary rolespermission denied for schema public
42501insufficient_privilegepermission denied for sequence …INSERT granted on a serial table, but not USAGE on its sequencepermission denied for sequence
42501insufficient_privilegepermission denied to create extension ”…”the extension is not marked trusted, or the role lacks CREATE on the databasecreate extension
42501insufficient_privilegemust be owner of table …ALTER, DROP or CREATE INDEX by a role that only has grantsmust be owner of table
42501insufficient_privilegenew row violates row-level security policy for table …the row fails the policy’s WITH CHECK, or an upsert also needs a SELECT policynew row violates
42501insufficient_privilegequery would be affected by row-level security policy for table …pg_dump (which sets row_security = off) run as a role subject to row-level securitydebugging row-level security
42P01undefined_tablerelation ”…” does not existcapital letters, a schema not on the search path, or no USAGE on the schemarelation does not exist
42703undefined_columncolumn ”…” does not exista column created with capitals, or a string in double quotescolumn does not exist
42704undefined_objectunrecognized configuration parameter ”…”the name tells which of five: a newer dump (transaction_timeout), a removed setting, a MySQL or psql command, or a custom setting read before it was setunrecognized configuration parameter
42710duplicate_objectpolicy ”…” for table ”…” already existsa migration that runs CREATE POLICY twiceCREATE POLICY IF NOT EXISTS
42803grouping_errorcolumn ”…” must appear in the GROUP BY clause or be used in an aggregate functiona selected column that is neither grouped nor aggregated, or grouped by a unique column or foreign key instead of the primary keymust appear in the GROUP BY clause
42804datatype_mismatchcolumn ”…” is of type jsonb but expression is of type character varying (or text)a typed text or varchar parameter, such as JDBC setString, where a literal would have workedcolumn is of type … but expression is of type
42883undefined_functionoperator does not exist: uuid = textcurrent_setting() or a text value compared with a uuid columnuuid = text
42P10invalid_column_referencethere is no unique or exclusion constraint matching the ON CONFLICT specificationthe conflict target does not match the index, often per tenant or partialON CONFLICT
42P17invalid_object_definitioninfinite recursion detected in policy for relation ”…”a policy that queries its own tableinfinite recursion
428C9generated_alwayscannot insert a non-DEFAULT value into column “id”an explicit id (or NULL) for a generated always as identity columnnull value in column “id”
53200out_of_memoryout of shared memory (hint: max_locks_per_transaction)one transaction locking thousands of tables or partitionsout of shared memory
53300too_many_connectionsremaining connection slots are reserved for …too many application connections, connections nobody closes, or sessions idle in transaction — also as sorry, too many clients alreadyremaining connection slots
54001statement_too_complexstack depth limit exceededa trigger, function or policy that calls itselfstack depth limit exceeded
55006object_in_usedatabase ”…” is being accessed by other users (or source database ”…”, for CREATE DATABASE … TEMPLATE)a pool, an idle client or a GUI still connected to the database or its templatedatabase is being accessed by other users
55P03lock_not_availablecanceling statement due to lock timeout (or could not obtain lock on row, with NOWAIT)a migration or update waiting longer than lock_timeoutlock timeout
57014query_canceledcanceling statement due to statement timeout (or … due to user request)statement_timeout reached, waiting for a lock or working; user request is pg_cancel_backend() or a driver’s query timeoutstatement timeout
57P01admin_shutdownterminating connection due to administrator commandpg_terminate_backend() on the session, for example on one idle in transactionidle in transaction

Several codes come back in more than one form; two matter for error handling. 40001 is both a serialization failure on the primary and a query cancelled on a replica: both mean “run the transaction again”, but the replica’s version can arrive as a dropped connection. And 42501 covers every missing privilege as well as row-level security refusing a row, so a retry loop or an error handler that branches on the code alone cannot tell “grant missing” from “policy refused this row” — read the message too.

What the class of a SQLSTATE — its first two characters — says about what to do

Which class is the error in?

  • 40 — transaction rollback (40001, 40P01)Retry the whole transactionFrom its start, not the statement; a savepoint retry deadlocked again in our test.
  • 25P02 — current transaction is abortedLook for the error before itThe real cause came earlier in the same transaction; roll back. (25P03, 25P04: the connection is gone — reconnect, and find the code that keeps transactions open.)
  • 42 — syntax or access (42501, 42P01, 42703…)Usually a fix in the code, schema or grantsIf a retry does succeed — 42P01 after another session's commit, or on a different pooled connection — the cause is session state.
  • 22 or 23 — data or integrityFix the data or the statementExcept 23505 on a _pkey after an import: fix the sequence.
  • 53, 54, 55 — resources and object stateDepends on the code53200: raise max_locks_per_transaction and restart. 53300: find what holds the connections. 54001: find the loop; a higher limit only moved it. 55P03: keep lock_timeout short and retry the whole transaction.

Class meanings from PostgreSQL's error code list; the advice per class from the measurements in the articles linked above.

How do you read the SQLSTATE of an error?

In psql, \set VERBOSITY verbose prints it in front of the message (ERROR: 42501: permission denied for table invoices). psycopg2 puts it in e.pgcode and psycopg 3 in e.sqlstate (e.diag.sqlstate works in both) and map most codes to an exception class: UniqueViolation for 23505, InFailedSqlTransaction for 25P02, InvalidColumnReference for 42P10, StringDataRightTruncation for 22001. In the server log it is not shown by default; add %e to log_line_prefix. A message that comes before any server has answered, such as Connection refused … Is the server running on that host, has no server SQLSTATE at all; it is libpq’s, see connection refused.

What we did not measure

Connection errors in class 08 (08006 connection failure and friends), authentication errors in class 28, and errors from extensions. The table only lists errors we reproduced ourselves; PostgreSQL defines several hundred codes.

Sources: PostgreSQL 17 documentation, Appendix A, PostgreSQL error codes, read on 22 October 2026. The causes and SQLSTATEs in the table are from our own measurements, linked per row; 42P17, 42501 for query would be affected and 25P01 were re-checked, and 57014, 25P04, 57P01, 428C9, 0A000 and 25006 confirmed in the linked articles, on PostgreSQL 17.11 on 22 October 2026.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation