PostgreSQL Error Codes (SQLSTATE) Explained with Fixes
Chat2DB TeamEvery error PostgreSQL raises carries a five-character SQLSTATE code alongside the human-readable message. The message is what you read; the code is what your application should branch on. Messages change wording between versions and get translated by lc_messages; codes do not. If your retry logic matches on the string "deadlock detected", it will silently stop working on a server configured in German. If it matches on 40P01, it works everywhere.
This guide covers how SQLSTATE codes are structured, how to get at them from psql and the common client libraries, what each class means, and the specific codes you will run into in day-to-day work, with the exact message and the fix for each.
What a SQLSTATE is
A SQLSTATE is five characters: a two-character class followed by a three-character subclass. Characters are digits or upper-case letters. The class tells you the category of the problem; the subclass narrows it down.
2 3 5 0 5
└─┬─┘ └─┬─┘
class subclass
23 = integrity constraint violation
505 = unique_violationA subclass of 000 means "the generic error for this class". So 23000 is an unspecified integrity constraint violation, while 23505 is specifically a unique violation. Codes whose subclass starts with a letter (42P01, 40P01, 25P02) are PostgreSQL-specific extensions to the SQL standard. Codes whose class starts with a letter that the standard does not use (P0, XX) are entirely PostgreSQL's own.
Each code also has a condition name such as unique_violation or undefined_table. These names are what you use in PL/pgSQL exception handlers and what client libraries like psycopg turn into exception class names. The full list lives in Appendix A of the PostgreSQL manual and in src/backend/utils/errcodes.txt in the source tree.
How to see the code
psql hides the SQLSTATE by default. Turn on verbose mode and it appears in front of the message, together with the source file and line where the error was raised:
psql -d appdb\set VERBOSITY verbose
INSERT INTO users (email) VALUES ('a@b.com');ERROR: 23505: duplicate key value violates unique constraint "users_email_key"
DETAIL: Key (email)=(a@b.com) already exists.
SCHEMA NAME: public
TABLE NAME: users
CONSTRAINT NAME: users_email_key
LOCATION: _bt_check_unique, nbtinsert.c:666If you have already hit an error in default mode, \errverbose reprints the last error with the verbose fields, so you do not need to re-run the statement.
In PL/pgSQL the code is available as the SQLSTATE variable inside an EXCEPTION block, and the rest of the diagnostics come from GET STACKED DIAGNOSTICS:
DO $$
DECLARE
v_state text;
v_msg text;
v_detail text;
v_constraint text;
BEGIN
INSERT INTO users (email) VALUES ('a@b.com');
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
v_state = RETURNED_SQLSTATE,
v_msg = MESSAGE_TEXT,
v_detail = PG_EXCEPTION_DETAIL,
v_constraint = CONSTRAINT_NAME;
RAISE NOTICE 'state=% constraint=% msg=% detail=%',
v_state, v_constraint, v_msg, v_detail;
END $$;Client libraries expose the same field:
| Library | Where the code lives |
|---|---|
| psycopg 3 (Python) | e.sqlstate; typed classes in psycopg.errors (UniqueViolation, DeadlockDetected, ...); e.diag.constraint_name |
| psycopg2 (Python) | e.pgcode; typed classes in psycopg2.errors |
node-postgres (pg) | err.code, plus err.detail, err.constraint, err.table |
| JDBC | SQLException.getSQLState(); PSQLException.getServerErrorMessage() for detail and constraint |
| pgx (Go) | errors.As(err, &pgErr) then pgErr.Code, pgErr.ConstraintName |
| libpq (C) | PQresultErrorField(res, PG_DIAG_SQLSTATE) |
The pattern is the same everywhere: match on the code, log the message.
Chat2DB also has a free PostgreSQL error code lookup (opens in a new tab) that explains any SQLSTATE when you just want to paste one in and read what it means.
The class map
The first two characters are the fastest way to triage an unfamiliar code:
| Class | Meaning | Typical members |
|---|---|---|
00 | Successful completion | 00000 |
01 | Warning | 01000, 01003 null value eliminated in set function |
02 | No data | 02000 (cursor exhausted, SELECT INTO found nothing) |
08 | Connection exception | 08006 connection failure, 08001 client could not connect, 08P01 protocol violation |
0A | Feature not supported | 0A000 |
22 | Data exception | 22P02 invalid text representation, 22001 string too long, 22012 division by zero, 22003 numeric out of range, 22007 invalid datetime format |
23 | Integrity constraint violation | 23505, 23503, 23502, 23514, 23P01 exclusion violation |
25 | Invalid transaction state | 25P02 failed transaction, 25001 active transaction, 25006 read-only |
28 | Invalid authorization | 28000 invalid authorization, 28P01 bad password |
3D | Invalid catalog name | 3D000 database does not exist |
3F | Invalid schema name | 3F000 schema does not exist |
40 | Transaction rollback | 40001 serialization failure, 40P01 deadlock |
42 | Syntax error or access rule violation | 42601, 42P01, 42703, 42883, 42501, 42P07 |
53 | Insufficient resources | 53300 too many connections, 53100 disk full, 53200 out of memory |
54 | Program limit exceeded | 54000, 54001 statement too complex, 54011 too many columns |
55 | Object not in prerequisite state | 55P03 lock not available, 55006 object in use, 55000 |
57 | Operator intervention | 57014 query canceled, 57P01 admin shutdown, 57P03 cannot connect now |
58 | System error | 58030 I/O error, 58P01 undefined file |
P0 | PL/pgSQL error | P0001 raise_exception, P0002 no_data_found, P0003 too_many_rows |
XX | Internal error | XX000 internal_error, XX001 data_corrupted, XX002 index_corrupted |
A useful rule of thumb: classes 22, 23 and 42 are bugs in the SQL or the data you sent and retrying will not help. Class 40 is designed to be retried. Classes 53, 57 and 08 are about the server or the connection, and the fix is usually operational rather than in the query.
The codes you will actually meet
23505 unique_violation
ERROR: duplicate key value violates unique constraint "users_email_key"
DETAIL: Key (email)=(a@b.com) already exists.A row with the same value already exists under a unique constraint or index. Either the data really is duplicated, the sequence is behind the data after a manual insert or restore, or two sessions raced. For an insert that should tolerate existing rows, use an upsert:
INSERT INTO users (email, name)
VALUES ('a@b.com', 'Ann')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;The causes and fixes are covered in depth in Fix "duplicate key value violates unique constraint".
23503 foreign_key_violation
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL: Key (customer_id)=(42) is not present in table "customers".Or, from the other side, when deleting a parent that still has children:
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(42) is still referenced from table "orders".Insert the parent first, or decide the referential action once at schema time instead of handling it in every code path:
ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey,
ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE;23502 not_null_violation
ERROR: null value in column "email" of relation "users" violates not-null constraint
DETAIL: Failing row contains (7, null, Ann).Supply the value, add a DEFAULT, or drop the constraint if it was wrong:
ALTER TABLE users ALTER COLUMN email SET DEFAULT '';
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;The DETAIL line lists the whole failing row in column order, which is often faster than reading the INSERT.
23514 check_violation
ERROR: new row for relation "products" violates check constraint "products_price_check"
DETAIL: Failing row contains (12, Widget, -5.00).Look up the constraint definition rather than guessing at it:
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'products'::regclass AND contype = 'c';42P01 undefined_table
ERROR: relation "users" does not exist
LINE 1: SELECT * FROM users;
^The table is misspelled, in a schema not on search_path, in a different database, or was created with a quoted mixed-case name. Check the last two:
SHOW search_path;
SELECT table_schema, table_name FROM information_schema.tables
WHERE table_name ILIKE 'users';If the result shows "Users", the table was created quoted and must always be referenced as "Users".
42703 undefined_column
ERROR: column "emial" does not exist
LINE 1: SELECT emial FROM users;
^
HINT: Perhaps you meant to reference the column "users.email".Same causes as above at the column level. A common variant is using double quotes where you meant a string literal: WHERE name = "Ann" is a column reference to Ann, not the text 'Ann'.
42P07 duplicate_table
ERROR: relation "users" already existsUsually a migration re-running. Make DDL idempotent with CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, and DROP ... IF EXISTS.
42601 syntax_error
ERROR: syntax error at or near "FORM"
LINE 1: SELECT * FORM users;
^The caret marks the first token the parser could not accept, which is often one token after the real mistake. Trailing commas before FROM, a missing closing parenthesis, and reserved words used as identifiers (user, order, group) without quotes account for most of these.
42501 insufficient_privilege
ERROR: permission denied for table usersGrant the privilege to the role, or to a group role the user belongs to. Remember that GRANT applies to existing objects only; use ALTER DEFAULT PRIVILEGES for tables created later:
GRANT SELECT, INSERT, UPDATE ON users TO app_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app_ro;42883 undefined_function
ERROR: function date_trunc(text, integer) does not exist
HINT: No function matches the given name and argument types. You might need to add explicit type casts.The function exists but not for those argument types, or an extension is not installed. The related operator does not exist: integer = text also carries 42883. Cast explicitly, or install the extension:
SELECT date_trunc('day', to_timestamp(1700000000));
CREATE EXTENSION IF NOT EXISTS pgcrypto;22P02 invalid_text_representation
ERROR: invalid input syntax for type integer: "abc"A string that cannot be parsed as the target type, often from a parameter bound as text or a CSV column. Validate at the boundary, or test conversion without raising (PostgreSQL 16 and later):
SELECT pg_input_is_valid('abc', 'integer'); -- false22001 string_data_right_truncation
ERROR: value too long for type character varying(50)PostgreSQL never silently truncates. Widen the column, or drop the length limit entirely, which costs nothing in PostgreSQL:
ALTER TABLE users ALTER COLUMN name TYPE text;40001 serialization_failure
ERROR: could not serialize access due to concurrent updateor under SERIALIZABLE:
ERROR: could not serialize access due to read/write dependencies among transactions
HINT: The transaction might succeed if retried.Not a bug. The isolation level did its job and the transaction must be retried from the beginning. See the retry section below.
40P01 deadlock_detected
ERROR: deadlock detected
DETAIL: Process 5120 waits for ShareLock on transaction 9021; blocked by process 5133.
Process 5133 waits for ShareLock on transaction 9020; blocked by process 5120.
HINT: See server log for query details.Two transactions took locks in opposite orders. Retry the victim, then fix the ordering. A full walkthrough is in How to fix ERROR: deadlock detected.
53300 too_many_connections
FATAL: sorry, too many clients already
FATAL: remaining connection slots are reserved for non-replication superuser connectionsBoth messages share the code. Find who is holding the slots before raising max_connections:
SELECT usename, application_name, state, count(*)
FROM pg_stat_activity GROUP BY 1, 2, 3 ORDER BY 4 DESC;Sessions sitting in idle in transaction are the usual culprit; see idle in transaction and the max_connections guide.
57014 query_canceled
ERROR: canceling statement due to statement timeout
ERROR: canceling statement due to user requestThe first comes from statement_timeout, the second from pg_cancel_backend() or Ctrl-C in psql. Raise the timeout for the one session that needs it rather than globally:
SET statement_timeout = '5min';statement_timeout in PostgreSQL covers where to set it and what value to use.
25P02 in_failed_sql_transaction
ERROR: current transaction is aborted, commands ignored until end of transaction blockAn earlier statement in this transaction failed and you kept sending commands. PostgreSQL will refuse everything until ROLLBACK. The real error is the previous one in your log. If you need to survive a failing statement mid-transaction, wrap it in a SAVEPOINT and ROLLBACK TO SAVEPOINT on failure.
28P01 invalid_password
FATAL: password authentication failed for user "app"Wrong password, or the role exists but pg_hba.conf matched a rule with a method the client cannot satisfy. Check which rule matched with pg_hba_file_rules and confirm the password encryption method (scram-sha-256 versus md5) agrees between server and client.
3D000 invalid_catalog_name
FATAL: database "appdb" does not existThe database name in the connection string is wrong, or the connection went to a different server than you thought. psql -l lists the databases actually present.
Catching codes in PL/pgSQL
Exception handlers accept either the condition name or the literal code:
CREATE OR REPLACE FUNCTION register_user(p_email text, p_name text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE
v_id bigint;
BEGIN
INSERT INTO users (email, name) VALUES (p_email, p_name)
RETURNING id INTO v_id;
RETURN v_id;
EXCEPTION
WHEN unique_violation THEN
SELECT id INTO v_id FROM users WHERE email = p_email;
RETURN v_id;
WHEN SQLSTATE '23503' THEN
RAISE EXCEPTION 'referenced row missing: %', SQLERRM
USING ERRCODE = 'foreign_key_violation';
END $$;Two things to know. A block with an EXCEPTION clause runs inside a subtransaction, which costs a savepoint per call; do not wrap every statement in one in a hot loop. And you can catch a whole class at once: a class-level condition such as WHEN integrity_constraint_violation (or its code, WHEN SQLSTATE '23000') matches every 23xxx error, not just the generic one.
When you raise your own errors, give them a code so callers can match on it:
RAISE EXCEPTION 'balance would go negative'
USING ERRCODE = 'check_violation', HINT = 'top up first';Retrying 40001 and 40P01
Serialization failures and deadlocks are the two codes PostgreSQL explicitly expects you to retry. The retry must restart the whole transaction, not just the failed statement: after either error the transaction is aborted, and any reads it did before the failure may be stale. Catching 40001 inside a PL/pgSQL function does not help for the same reason, since the outer transaction is what needs to restart.
import random
import time
import psycopg
from psycopg.errors import DeadlockDetected, SerializationFailure
def transfer(conn, src, dst, amount):
with conn.transaction():
conn.execute(
"UPDATE accounts SET balance = balance - %s WHERE id = %s",
(amount, src),
)
conn.execute(
"UPDATE accounts SET balance = balance + %s WHERE id = %s",
(amount, dst),
)
def with_retry(fn, attempts=5):
for attempt in range(1, attempts + 1):
try:
with psycopg.connect("dbname=appdb", autocommit=True) as conn:
conn.isolation_level = psycopg.IsolationLevel.SERIALIZABLE
return fn(conn)
except (SerializationFailure, DeadlockDetected) as e:
if attempt == attempts:
raise
delay = 0.05 * (2 ** attempt) + random.uniform(0, 0.05)
print(f"attempt {attempt} failed with {e.sqlstate}, retrying in {delay:.2f}s")
time.sleep(delay)
with_retry(lambda c: transfer(c, 1, 2, 100))Exponential backoff with jitter matters: if every retrying client waits the same fixed interval, they collide again on the next attempt. Cap the attempt count, and make sure the work inside the transaction has no side effects outside the database (sending an email, calling an API) that would be repeated on retry.
When you are working through a set of failing statements interactively, a client that shows the SQLSTATE, DETAIL and HINT next to the result helps. Chat2DB surfaces the full error diagnostics for PostgreSQL and can explain them inline; download it at chat2db.ai/download (opens in a new tab) or use it in the browser at app.chat2db.ai (opens in a new tab).
Summary
SQLSTATE codes are the stable, language-independent identity of a PostgreSQL error: two characters of class, three of subclass. Turn on \set VERBOSITY verbose in psql, read SQLSTATE and GET STACKED DIAGNOSTICS in PL/pgSQL, and use e.sqlstate, err.code or getSQLState() in application code. Classes 22, 23 and 42 mean the SQL or data is wrong and should not be retried; class 40 should always be retried from the start of the transaction with backoff; classes 53, 57 and 08 point at the server or the connection rather than the query. Match on the code, log the message, and your error handling will survive both version upgrades and a server whose locale is not English.
