PostgreSQL error 23505 (Unique Violation) explained: what it means, why it happens, and how to fix it — with copy-paste code examples.
Inspect the error detail for the exact constraint and conflicting key value. If that does not apply, decide between rejecting, updating, or skipping the duplicate row — the full checklist is below.
SQLSTATE: 23505
Official name: Unique Violation
System name: unique_violation
Service: PostgreSQL
A unique constraint or exclusion constraint was violated while inserting or updating a row, because a conflicting row already exists.
You may see this error reported as one of the following messages:
duplicate key value violates unique constraint "users_email_key"Key (email)=(a@b.com) already exists.there is no unique constraint matching given keys for referenced tableA users table has UNIQUE(email) and a sequence-backed id. On signup, the app runs INSERT INTO users (email, name) VALUES ('john@example.com', 'John') — but that email already exists from an earlier signup that was soft-deleted. PostgreSQL aborts the statement with SQLSTATE 23505 and detail: Key (email)=(john@example.com) already exists. The same error appears when a row was inserted with an explicit id (say 500) above the sequence position, so the next auto-generated id collides.
Run these to confirm the cause before applying a fix — each command answers a specific question.
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'users'::regclass AND contype = 'u';
Returns the exact unique constraint name and column list — match it against the key named in the error detail.
SELECT id, email, created_at FROM users
WHERE email = 'john@example.com';
The conflicting row is usually a soft-deleted or duplicated account — decide whether to reactivate it or reject the insert.
SELECT last_value, (SELECT MAX(id) FROM users) AS max_id
FROM users_id_seq;
If last_value < max_id, an explicit-id insert desynchronized the sequence; fix it with setval().
Duplicate value is intentional (upsert)?
├─ YES → use INSERT ... ON CONFLICT (email) DO UPDATE
│ (atomic, no race window)
└─ NO → find the conflicting row
├─ existing row is valid → fix/reject the application input
└─ existing row is stale/duplicate → clean the data, then retry
Duplicate is the auto-generated id itself?
└─ sequence out of sync → SELECT setval('users_id_seq',
COALESCE(MAX(id),0)+1, false) FROM users;
INSERT INTO users (email, name)
VALUES ('a@b.com', 'Ada')
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name
RETURNING id;
ON CONFLICT converts a would-be unique violation into an atomic update, avoiding error 23505 entirely.
SELECT setval('users_id_seq', COALESCE(MAX(id), 0) + 1, false)
FROM users;
When a manually-set id exceeds the sequence value, the next auto-generated id collides; reset the sequence above MAX(id).
Catch PrismaClientKnownRequestError with code P2002 (maps to SQLSTATE 23505) and return a clean duplicate response instead of crashing.
try {
await prisma.user.create({ data: { email } });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
return { error: 'Email already exists' };
}
throw e;
}
Use get_or_create() or catch IntegrityError (django.db) to handle duplicates at the ORM layer.
from django.db import IntegrityError
try:
obj = MyModel.objects.create(email=email)
except IntegrityError:
obj = MyModel.objects.get(email=email)
Catch sqlalchemy.exc.IntegrityError, optionally with savepoint rollback before retrying.
from sqlalchemy.exc import IntegrityError
try:
session.add(User(email=email))
session.commit()
except IntegrityError:
session.rollback()
Use upsert() with onConflict to let Postgres handle duplicates server-side instead of returning 23505.
const { error } = await supabase
.from('users')
.upsert({ email }, { onConflict: 'email' })
Most often this happens when inserting a duplicate value into a column with a UNIQUE constraint, or when using INSERT ... ON CONFLICT DO NOTHING but the conflict clause omits the right column.
Inspect the error detail for the exact constraint and conflicting key value.
This page documents fixes for: prisma, django, sqlalchemy, supabase.
Recommendations are editorial — DB Error Reference takes no payment or affiliate fees for tool listings.
This page is based on the official PostgreSQL documentation linked below and adds practical troubleshooting guidance on top.