Fix: Prisma Unique Constraint Failed on the Fields
Quick Answer
How to fix Prisma's 'Unique constraint failed on the fields' error — finding which field caused it, handling upserts, race conditions, and bulk insert deduplication.
The Error
A Prisma create or update operation throws a unique constraint error:
PrismaClientKnownRequestError:
Unique constraint failed on the fields: (`email`)
code: 'P2002',
meta: { target: ['email'] }Or with a composite unique constraint:
PrismaClientKnownRequestError:
Unique constraint failed on the fields: (`userId`, `postId`)
code: 'P2002',
meta: { target: ['userId', 'postId'] }The operation attempted to insert or update a record where the unique field value already exists in the database.
Why This Happens
Prisma propagates the database’s unique constraint violation as error code P2002. This occurs when:
- Duplicate record creation — you call
prisma.user.create()with an email that’s already in the database. - No existence check before insert — the code doesn’t verify whether a record exists before trying to create it.
- Race condition — two concurrent requests both check “does this record exist?” get “no”, then both try to create it. One succeeds, one fails.
- Seed data run twice — a database seeder that doesn’t check for existing records fails on the second run.
- Composite unique constraint — a combination of fields (e.g.,
userId + postIdfor a join table) must be unique, and a duplicate combination was inserted. - Case sensitivity —
[email protected]and[email protected]might be treated as the same by your app but stored as different strings, causing unexpected duplicates when constraints are case-insensitive.
Fix 1: Catch P2002 and Return a Meaningful Error
At minimum, catch the Prisma error and return a user-friendly response instead of a 500:
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
async function createUser(data: { email: string; name: string }) {
try {
return await prisma.user.create({ data });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
const field = (e.meta?.target as string[])?.join(', ') ?? 'field';
throw new Error(`A user with this ${field} already exists.`);
}
}
throw e;
}
}In an Express route handler:
app.post('/users', async (req, res) => {
try {
const user = await createUser(req.body);
res.status(201).json(user);
} catch (e) {
if (e instanceof Error && e.message.includes('already exists')) {
res.status(409).json({ error: e.message }); // 409 Conflict
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});Why 409 not 400? HTTP 409 Conflict is the correct status code for a uniqueness violation — the request was valid, but it conflicts with the current state of the resource. 400 Bad Request implies the input was malformed.
Fix 2: Use upsert Instead of create
If the intent is “create if not exists, update if exists”, use upsert — it handles duplicates atomically:
const user = await prisma.user.upsert({
where: { email: data.email },
update: {
name: data.name, // Fields to update if record exists
updatedAt: new Date(),
},
create: {
email: data.email, // Fields to set when creating
name: data.name,
},
});For join tables (many-to-many), upsert handles the composite key:
const favorite = await prisma.userPostFavorite.upsert({
where: {
userId_postId: { // Prisma generates this compound key
userId: req.user.id,
postId: req.params.postId,
},
},
update: {}, // Nothing to update — just ensure it exists
create: {
userId: req.user.id,
postId: req.params.postId,
},
});upsert is not always appropriate. If creating a duplicate should be an error (e.g., a user trying to register with an existing email), catch the P2002 and return a 409 instead of silently updating the existing record.
Fix 3: Check Before Creating (with Caveats)
For cases where you want to explicitly verify uniqueness before inserting:
async function registerUser(email: string, name: string) {
const existing = await prisma.user.findUnique({
where: { email },
});
if (existing) {
throw new Error('Email already registered');
}
return await prisma.user.create({
data: { email, name },
});
}Caveat: This check-then-insert pattern has a race condition. Two concurrent requests can both pass the findUnique check and then both attempt create. In low-traffic scenarios this is acceptable, but for high-concurrency code (login endpoints, payment processing), use a database transaction or rely on catching P2002:
// Safer: attempt create, catch the uniqueness error
async function registerUser(email: string, name: string) {
try {
return await prisma.user.create({ data: { email, name } });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new Error('Email already registered');
}
throw e;
}
}This approach is both correct under concurrency and doesn’t require an extra database round-trip.
Fix 4: Fix Seeder Scripts
Seed scripts that run create on every execution fail the second time. Use upsert or createMany with skipDuplicates:
// prisma/seed.ts
// Option A: upsert (works for updating existing seed data too)
await prisma.role.upsert({
where: { name: 'admin' },
update: {},
create: { name: 'admin', permissions: ['read', 'write', 'delete'] },
});
// Option B: createMany with skipDuplicates (faster for large seeds)
await prisma.tag.createMany({
data: [
{ name: 'javascript' },
{ name: 'typescript' },
{ name: 'python' },
],
skipDuplicates: true, // Silently skips records that violate unique constraints
});skipDuplicates: true is particularly useful for reference data (countries, currencies, permissions) that should be idempotent — safe to run multiple times.
Fix 5: Handle Bulk Imports
When importing data from a CSV or external source, duplicates in the source data or against existing records cause failures:
async function importUsers(users: { email: string; name: string }[]) {
const results = { created: 0, skipped: 0, errors: [] as string[] };
for (const user of users) {
try {
await prisma.user.create({ data: user });
results.created++;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
results.skipped++; // Already exists — skip
} else {
results.errors.push(`${user.email}: ${(e as Error).message}`);
}
}
}
return results;
}For large imports, use createMany with skipDuplicates for better performance:
async function bulkImportUsers(users: { email: string; name: string }[]) {
const result = await prisma.user.createMany({
data: users,
skipDuplicates: true,
});
return { created: result.count, total: users.length };
}Note: createMany with skipDuplicates uses a single SQL statement, which is dramatically faster than individual inserts in a loop for large datasets.
Fix 6: Handle Unique Constraints on Optional Fields
Unique constraints on nullable fields behave differently than you might expect. In most databases (PostgreSQL, MySQL), NULL values are not considered equal for uniqueness purposes — you can have multiple rows with NULL in a unique column.
Prisma schema example:
model User {
id Int @id @default(autoincrement())
email String @unique
username String? @unique // Nullable unique field
}If your business logic requires that non-null values be unique but allows multiple nulls, this is correct. But if you expect null to be treated as unique (only one null allowed), you need a partial unique index — which must be added via a raw migration, not the Prisma schema DSL:
-- In a Prisma migration SQL file
CREATE UNIQUE INDEX users_username_unique
ON "User" (username)
WHERE username IS NOT NULL;Fix 7: Debug Which Constraint Is Failing
The meta.target field in the error tells you exactly which field (or fields) caused the violation:
try {
await prisma.user.create({ data });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
console.log('Failing fields:', e.meta?.target);
// e.g., ['email'] or ['userId', 'postId']
}
}Cross-reference with your Prisma schema:
model User {
id Int @id @default(autoincrement())
email String @unique // Single field constraint
@@unique([firstName, lastName]) // Composite constraint
}If target shows ['firstName', 'lastName'], the combination of first + last name already exists.
Still Not Working?
Check whether the unique index exists in the database. If you added @unique to the schema but didn’t run prisma migrate dev, the database doesn’t have the constraint:
npx prisma migrate dev # Apply pending migrations
npx prisma db push # Or push schema directly (dev only)Check if data was manually inserted into the database that violates constraints added later. If you added a unique constraint to a column that already has duplicates, the migration will fail:
# In psql — find duplicate emails before adding the constraint
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;Deduplicate the data before running the migration.
Verify @@unique placement. A common mistake is placing @@unique inside a field definition instead of at the model level:
model Post {
id Int @id @default(autoincrement())
userId Int
categoryId Int
// Wrong — @unique here makes categoryId alone unique
// categoryId Int @unique
// Correct — composite unique at model level
@@unique([userId, categoryId])
}For related Prisma issues, see Fix: Prisma Migration Failed and Fix: Postgres Relation Does Not Exist.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Prisma Enum Not Working — Invalid Enum Value or Enum Not Recognized
How to fix Prisma enum errors — schema definition, database sync, TypeScript enum type mismatch, filtering by enum, and migrating existing enum values.
Fix: Prisma Transaction Error — Transaction Already Closed or Rolled Back
How to fix Prisma transaction errors — interactive transactions vs $transaction array, error handling and rollback, nested transactions, timeout issues, and isolation levels.
Fix: Drizzle ORM Not Working — Schema Out of Sync, Relation Query Fails, or Migration Error
How to fix Drizzle ORM issues — schema definition, drizzle-kit push vs migrate, relation queries with, transactions, type inference, and common PostgreSQL/MySQL configuration problems.
Fix: Prisma Connection Pool Exhausted — Can't Acquire Connection from Pool
How to fix Prisma connection pool errors — pool size configuration, connection leaks, serverless deployments, singleton pattern, query timeout, and pgBouncer integration.