The error, verbatim
Jump to the fix ↓PrismaClientKnownRequestError:
Invalid `prisma.user.create()` invocation:
Unique constraint failed on the fields: (`email`)
Tested on
- prisma
- 7.10.1 (and 6.19.3 for comparison)
- Adapters
- @prisma/adapter-pg, @prisma/adapter-better-sqlite3 7.10.1
- Databases
- Postgres 18 (Docker), SQLite
- Node / OS
- 24.21.0 / Ubuntu 24.04
✓ Last re-run on GitHub Actions · see the run · all repros
Contents
If your “that email is already taken” message stopped appearing after upgrading to Prisma 7, this is why: on Prisma 7.10.1, e.meta.target was undefined on both Postgres and SQLite, and on Postgres the message names the constraint (User_email_key) instead of the field. The error code is still P2002, so nothing else looks wrong. P2002 itself means the database refused a row because a @unique or @id column already has that value.
Prisma 7 made the first question harder. On the same duplicate email, three setups gave three different results:
| Setup | Last line of the message | e.meta.target |
|---|---|---|
| Prisma 6.19.3, Postgres | Unique constraint failed on the fields: (`email`) |
["email"] |
| Prisma 7.10.1, Postgres (adapter-pg) | Unique constraint failed on the constraint: `User_email_key` |
undefined |
| Prisma 7.10.1, SQLite (adapter-better-sqlite3) | Unique constraint failed on the fields: (`email`) |
undefined |
So if your error handler reads e.meta.target to tell the user “that email is taken”, it stopped working on Prisma 7, on both databases, and the error’s code is still P2002 so nothing else looks wrong.
Find which field collided
Prisma 7 runs every query through a driver adapter, and the details moved into the adapter’s error. The field or constraint name is here:
e.meta?.driverAdapterError?.cause?.constraintOn Postgres that object was { "index": "User_email_key" }, the constraint’s name. On SQLite it was { "fields": ["email"] }. A helper that works on both, and on Prisma 6:
import { Prisma } from './generated/prisma/client';
const isUnique = (e: unknown) =>
e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002';
const which = (e: any) => {
const c = e?.meta?.driverAdapterError?.cause?.constraint;
return c?.fields?.join(',') ?? c?.index ?? e?.meta?.target;
};Postgres names a unique constraint <Model>_<field>_key by default, and the primary key <Model>_pkey, so the field is still easy to read from the name.
Why your code wrote it twice
I hit P2002 five ways on one User model with a unique email:
| What the code did | Postgres | SQLite |
|---|---|---|
create with an email that exists |
P2002 | P2002 |
update to an email another row has |
P2002 | P2002 |
create after rows were inserted with explicit ids |
P2002 on User_pkey |
worked |
3 parallel upsert calls for the same new email, 20 times |
40 of 60 failed | 0 of 60 failed |
3 parallel “findUnique, then create if missing”, 20 times |
40 of 60 failed | 40 of 60 failed |
Also worth knowing: createMany with the same email twice in one call failed with P2002 and inserted nothing, not even the first row. And A@x.com next to a@x.com did not fail: the check is case-sensitive, so duplicates by case get through.
P2002 on the id: the sequence is behind
If the message names User_pkey (or fields: (`id`)), your new row got an id that already exists. On Postgres, @default(autoincrement()) takes the next number from a sequence. Rows inserted with an explicit id, by a seed script, an import or a copied dump, don’t move the sequence. So the next normal create asks for id 1, which a seed row already has.
SQLite picks the next id from the highest one in the table, so the same steps worked there.
Move the sequence past the highest id:
SELECT setval(pg_get_serial_sequence('"User"', 'id'), (SELECT MAX(id) FROM "User"));From Prisma, run it with prisma.$executeRawUnsafe(...), or in psql. Use your own table and column names. Prisma’s table names keep their capital letters, so they need the double quotes.
P2002 from upsert: two requests at once
This one confuses people, because upsert sounds like it can’t create a duplicate. On Postgres, 40 of my 60 parallel upserts failed with P2002: the requests all checked, all saw no row, and all tried to insert. SQLite ran the same test with 0 failures.
The same race hits any “check, then create” code, on both databases: a form submitted twice, a webhook delivered twice, two tabs signing up at once.
The database’s unique constraint is what keeps the data right, so let it, and treat P2002 as “someone else just created it”:
async function upsertUser(email: string) {
try {
return await prisma.user.upsert({ where: { email }, update: {}, create: { email } });
} catch (e) {
if (isUnique(e)) return prisma.user.findUniqueOrThrow({ where: { email } });
throw e;
}
}When you only need the rows to exist and don’t need them back, createMany with skipDuplicates: true skips duplicates instead of failing:
How this was tested
Ubuntu 24.04 on a GitHub Actions runner, Node 24.21.0, October 18, 2026. Prisma 7.10.1 with @prisma/adapter-pg against Postgres 18 in Docker, and with @prisma/adapter-better-sqlite3 against a SQLite file, both on the same User model. Prisma 6.19.3 without an adapter, on the same Postgres, for the comparison. The parallel tests sent 3 requests at once for each of 20 new emails. Every message above is the real output, from the test script.
— N.K., end of entry No.079