What Happens When You Don't Handle Prisma P2002 in Production
By Nark Team
When a user signs up with an email that already exists in your database, Prisma throws PrismaClientKnownRequestError with error code P2002. If your code does not catch this error, your Express/Next.js/Fastify handler crashes and the user sees a 500 Internal Server Error. They have no idea their email is already registered. They try again. Same 500. They leave. This is the most common Prisma-related production bug in TypeScript applications, and it is entirely preventable with a three-line catch block.
Quick Answer: Prisma's
P2002error code means a unique constraint was violated (duplicate email, duplicate username, etc.). CatchPrismaClientKnownRequestError, check forerror.code === 'P2002', and return a 409 with a helpful message. To find every unhandled P2002 in your codebase:npx nark --tsconfig ./tsconfig.json
The Incident
Here is a real pattern from a TypeScript SaaS application. The signup route:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
app.post('/api/signup', async (req, res) => {
const { email, password } = req.body;
const user = await prisma.user.create({
data: {
email,
password: await hash(password),
},
});
res.json({ userId: user.id });
});
This works for every new user. The first time someone signs up with alice@example.com, it succeeds. The second time, Prisma throws:
PrismaClientKnownRequestError:
Invalid `prisma.user.create()` invocation:
Unique constraint failed on the fields: (`email`)
at Object.request (/node_modules/@prisma/client/runtime/library.js:...)
code: 'P2002'
meta: { target: ['email'] }
Without a try-catch, Express catches this as an unhandled error and returns:
{
"status": 500,
"message": "Internal Server Error"
}
The user sees a generic error. They do not know their email is already registered. They cannot recover. Your monitoring fires. Your team investigates. The fix is three lines of code.
Why P2002 Happens
Prisma error code P2002 fires when an INSERT or UPDATE violates a unique constraint in your database. Common triggers:
| Prisma Operation | Unique Field | When P2002 Fires |
|---|---|---|
prisma.user.create() | email | User signs up with existing email |
prisma.user.create() | username | Username already taken |
prisma.organization.create() | slug | Org slug already exists |
prisma.apiKey.create() | key | Generated API key collides (rare) |
prisma.user.update() | email | User changes email to one already in use |
prisma.post.create() | [authorId, slug] | Compound unique: same author, same slug |
Every create() or update() on a model with unique fields can throw P2002.
The Fix: Catch P2002 Specifically
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
app.post('/api/signup', async (req, res) => {
const { email, password } = req.body;
try {
const user = await prisma.user.create({
data: {
email,
password: await hash(password),
},
});
res.status(201).json({ userId: user.id });
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
const target = (error.meta?.target as string[]) ?? [];
res.status(409).json({
error: 'conflict',
message: `An account with this ${target.join(', ')} already exists`,
});
return;
}
// Re-throw unexpected errors
throw error;
}
});
Now when a user signs up with an existing email:
{
"error": "conflict",
"message": "An account with this email already exists"
}
HTTP 409 Conflict. The user understands the problem. They can log in instead. No 500. No monitoring alert. No incident.
Other Prisma Errors You Should Handle
P2002 is the most common, but Prisma throws several other PrismaClientKnownRequestError codes that cause production issues:
P2025: Record Not Found
Thrown by update(), delete(), and findUniqueOrThrow() when the record does not exist.
try {
await prisma.user.update({
where: { id: userId },
data: { name: newName },
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
res.status(404).json({ error: 'User not found' });
return;
}
throw error;
}
P2003: Foreign Key Constraint Violation
Thrown when you reference a related record that does not exist.
try {
await prisma.post.create({
data: {
title: 'Hello',
authorId: userId, // this user doesn't exist
},
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2003'
) {
res.status(400).json({ error: 'Referenced author does not exist' });
return;
}
throw error;
}
PrismaClientInitializationError: Connection Failure
Thrown when Prisma cannot connect to the database at all.
try {
await prisma.$connect();
} catch (error) {
if (error instanceof Prisma.PrismaClientInitializationError) {
console.error('Database connection failed:', error.message);
process.exit(1);
}
throw error;
}
The "Check First" Anti-Pattern
Some developers try to avoid P2002 by checking for duplicates before inserting:
// Anti-pattern: race condition
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
return res.status(409).json({ error: 'Email already taken' });
}
// Between the findUnique and create, another request can insert the same email
const user = await prisma.user.create({ data: { email, password } });
This has a race condition. Two concurrent signup requests for the same email both pass the findUnique check, and one of them hits P2002 on the create. The correct approach is to attempt the insert and catch the constraint violation:
// Correct: catch the constraint violation
try {
const user = await prisma.user.create({ data: { email, password } });
return res.status(201).json({ userId: user.id });
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
return res.status(409).json({ error: 'Email already taken' });
}
throw error;
}
This is safe under concurrency. The database enforces the constraint atomically.
How Nark Catches Unhandled P2002
Nark's profile for @prisma/client checks whether create(), update(), upsert(), and delete() calls on Prisma models are wrapped in error handling that catches PrismaClientKnownRequestError. If not, Nark flags it:
ERROR @prisma/client prisma.user.create() without P2002 handling
src/routes/signup.ts:12 in signupHandler()
Prisma throws PrismaClientKnownRequestError on unique constraint violation
Fix: catch PrismaClientKnownRequestError, check error.code === 'P2002'
This catches the exact bug described in this article. Every create() call on a model with unique fields is a potential P2002 crash site.
npx nark --tsconfig ./tsconfig.json
The Cost of Not Handling P2002
In a production SaaS application, an unhandled P2002 causes:
- User confusion. "Internal Server Error" on a signup page. The user does not know their email is already registered.
- Lost signups. Users who see a 500 often leave and do not come back.
- Support tickets. "I can't sign up" tickets that your team investigates manually.
- Monitoring noise. Every duplicate signup attempt triggers a 500 error alert.
- Trust erosion. Users who see 500 errors assume the product is unstable.
The fix is a try-catch and a 409 response. Three lines of code. Five minutes of work. It prevents all five of these outcomes.
Frequently Asked Questions
Does Prisma always throw on duplicate records?
Yes. If your schema has a @unique or @@unique constraint, any create() or update() that violates it throws PrismaClientKnownRequestError with code P2002. There is no option to silently skip duplicates.
What about upsert?
prisma.user.upsert() does not throw P2002 because it handles the "already exists" case by updating instead of inserting. If your logic is "create or update," use upsert instead of create with a P2002 catch.
Does findUniqueOrThrow throw P2002?
No. findUniqueOrThrow throws PrismaClientKnownRequestError with code P2025 (not found), not P2002. P2002 is only for write operations that violate unique constraints.
Will ESLint catch a missing P2002 handler?
No. ESLint has no knowledge of Prisma error codes. It does not know that prisma.user.create() can throw PrismaClientKnownRequestError. Nark checks this specifically.
What about Prisma's $transaction?
Errors inside prisma.$transaction() also throw PrismaClientKnownRequestError. Wrap the transaction in a try-catch and handle P2002 the same way. If any operation inside the transaction fails, the entire transaction is rolled back.
Try It Now
npx nark --tsconfig ./tsconfig.json
Nark checks 160+ packages — including axios, prisma, stripe, and redis — for correct error handling, including Prisma's P2002, P2025, and connection errors. Run it on your project to find every unhandled Prisma error before it returns a 500 to your users.