Prisma ORMRunbook 0/4 done
P2002

Prisma ORM Error P2002: Unique constraint failed

Error✕ Not retryable · fix the payloadHTTP 409 ConflictDatabase
TL;DR · 10-second read

Prisma tried to write a value that already exists in a @unique or @@unique field (Postgres 23505 underneath). Catch P2002 and return 409, or use upsert.

Identify: does your log match?

What you see · exact log / response
PrismaClientKnownRequestError:
Invalid `prisma.user.create()` invocation in
/app/src/routes/signup.ts:18:34

  15 const hash = await argon2.hash(body.password);
  16
  17 // create account
→ 18 const user = await prisma.user.create(
Unique constraint failed on the fields: (`email`)
  code: 'P2002',
  meta: { modelName: 'User', target: [ 'email' ] }
  1. High

    Duplicate signup or double submit

    The user clicked twice or the client retried after a timeout. The first create already committed the email.

  2. Medium

    findFirst then create race

    Two requests both check for an existing row, find none, and both call create. Only the database constraint stops the second.

  3. Medium

    Seed script re-run on populated DB

    prisma db seed with create() instead of upsert() fails on the second run.

Fix

Catch P2002 and return a 409

✕ Broken · TypeScript
const user = await prisma.user.create({ data: { email, name } });
return Response.json(user);
✓ Fixed · TypeScript
import { Prisma } from '@prisma/client';

try {
  const user = await prisma.user.create({ data: { email, name } });
  return Response.json(user, { status: 201 });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
    return Response.json({ error: 'Email already registered' }, { status: 409 });
  }
  throw e;
}

The unique index is the source of truth; mapping P2002 to 409 gives clients a clear, non-retryable answer.

Verify: reproduce it on purpose

Reproduce locally · node
$ node -e "const {PrismaClient}=require('@prisma/client');const p=new PrismaClient();(async()=>{await p.user.create({data:{email:'dup@x.io'}});await p.user.create({data:{email:'dup@x.io'}})})().catch(e=>console.log(e.code))"

Run it against a dev environment to trigger P2002 on purpose and test your error handler.

Prevent

  • Read e.meta.target to know which field collided.
  • Disable the submit button while the request is in flight.
  • Prefer upsert in seed scripts.

FAQ

Why is meta.target an index name instead of field names?

On some databases and older Prisma versions, target is the constraint name (e.g. User_email_key). Parse both forms or match on the substring.

Can upsert still throw P2002?

Yes, if Prisma falls back to a non-native upsert (select then insert) and two calls race. Retry once on P2002 in that case.

Does P2002 roll back my $transaction?

Yes. Any thrown error inside an interactive $transaction rolls back all prior writes in it.

Related errors

Last verified 2026-10-20 · Source: Prisma error reference P1000–P6010 · Official docs ↗ · ~12,100 searches/mo