← Back to Blog

How to Handle Prisma Errors in TypeScript (P2002, P2025, and More)

By Nark Team

Prisma throws specific error classes with machine-readable error codes that tell you exactly what went wrong. The two most common are P2002 (unique constraint violation, like a duplicate email on signup) and P2025 (record not found on update or delete). You catch them by importing PrismaClientKnownRequestError from @prisma/client/runtime/library and matching the .code property in your catch block. Every prisma.user.create(), prisma.user.update(), and prisma.user.delete() call can throw these, and your code needs to handle each case explicitly.

Quick Answer: Import PrismaClientKnownRequestError from @prisma/client/runtime/library, wrap your Prisma calls in try-catch, and switch on error.code to handle P2002 (duplicate), P2025 (not found), and P2003 (foreign key) separately. To check that every Prisma call in your codebase has proper error handling: npx nark --tsconfig ./tsconfig.json


What Does Prisma Error Code P2002 Mean?

P2002 is Prisma's unique constraint violation error. According to the Prisma documentation, it fires whenever an insert or update would create a duplicate value in a column (or combination of columns) that has a unique constraint.

The most common trigger: a user signs up with an email that already exists in your database.

import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';

const prisma = new PrismaClient();

async function createUser(email: string, name: string) {
  try {
    const user = await prisma.user.create({
      data: { email, name },
    });
    return user;
  } catch (error) {
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      // error.meta.target contains the field(s) that caused the conflict
      const fields = (error.meta?.target as string[]) ?? [];
      throw new Error(`A user with that ${fields.join(', ')} already exists.`);
    }
    throw error;
  }
}

The key detail: error.meta.target is an array of the field names that violated the constraint. For a unique email, it would be ['email']. For a compound unique on [firstName, lastName], it would be ['firstName', 'lastName']. Use this to build a specific error message for your API response instead of leaking database internals to the client.

P2002 can also fire on update and upsert. If you update a user's email to one that already exists, you get the same error code. According to the Prisma documentation, concurrent upsert calls for the same non-existent record can also trigger P2002 when one call creates the record before the other.


What Is Prisma Error Code P2025 (Record Not Found)?

P2025 means Prisma tried to operate on a record that does not exist. This happens with update, delete, findUniqueOrThrow, and findFirstOrThrow.

async function updateUserEmail(userId: string, newEmail: string) {
  try {
    const user = await prisma.user.update({
      where: { id: userId },
      data: { email: newEmail },
    });
    return user;
  } catch (error) {
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === 'P2025'
    ) {
      throw new Error(`User ${userId} not found.`);
    }
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      throw new Error(`Email ${newEmail} is already taken.`);
    }
    throw error;
  }
}

A common source of P2025 bugs: calling prisma.user.delete({ where: { id } }) on an already-deleted record. If your delete endpoint should be idempotent (calling it twice returns success both times), catch P2025 and return a 200 instead of letting it crash.

Note the difference between findUnique and findUniqueOrThrow. findUnique returns null when no record matches. findUniqueOrThrow throws a PrismaClientKnownRequestError with code P2025. Choose based on whether you want null-checking or exception-based flow control.


How to Catch PrismaClientKnownRequestError?

Prisma has four error classes. Each one represents a different category of failure:

Error ClassImport PathWhen It Fires
PrismaClientKnownRequestError@prisma/client/runtime/libraryQuery engine recognized the error (has .code)
PrismaClientUnknownRequestError@prisma/client/runtime/libraryQuery engine could not classify the error
PrismaClientValidationError@prisma/client/runtime/libraryInvalid query (wrong field names, missing required data)
PrismaClientInitializationError@prisma/client/runtime/libraryConnection failed, engine could not start

The import path matters. This is a common mistake:

// Wrong - this does not export error classes
import { PrismaClientKnownRequestError } from '@prisma/client';

// Correct - error classes live in the runtime
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';

Here is the full pattern for catching all four:

import {
  PrismaClientKnownRequestError,
  PrismaClientUnknownRequestError,
  PrismaClientValidationError,
  PrismaClientInitializationError,
} from '@prisma/client/runtime/library';

async function safeQuery<T>(operation: () => Promise<T>): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    if (error instanceof PrismaClientKnownRequestError) {
      // Has .code (P2002, P2025, etc.) and .meta
      switch (error.code) {
        case 'P2002':
          throw new ConflictError('Duplicate record', error.meta);
        case 'P2025':
          throw new NotFoundError('Record not found');
        case 'P2003':
          throw new BadRequestError('Referenced record does not exist');
        case 'P2014':
          throw new BadRequestError('Required relation violation');
        default:
          throw new DatabaseError(`Prisma error ${error.code}: ${error.message}`);
      }
    }
    if (error instanceof PrismaClientValidationError) {
      // Bad query shape - this is a developer error, not a runtime error
      throw new BadRequestError(`Invalid query: ${error.message}`);
    }
    if (error instanceof PrismaClientInitializationError) {
      // Connection problem - retry may help
      throw new ServiceUnavailableError('Database connection failed');
    }
    if (error instanceof PrismaClientUnknownRequestError) {
      // Unknown database error
      throw new DatabaseError(`Unknown database error: ${error.message}`);
    }
    throw error;
  }
}

What Errors Does prisma.user.create() Throw?

A single prisma.user.create() call can throw four distinct error types. Here is the complete list:

Error CodeError ClassCauseWhat to Do
P2002PrismaClientKnownRequestErrorUnique constraint violation (duplicate value)Return 409 Conflict, tell user which field is duplicated
P2003PrismaClientKnownRequestErrorForeign key constraint (referenced record missing)Verify parent record exists before creating
N/APrismaClientValidationErrorMissing required field or wrong field typeFix the query, this is a code bug
N/APrismaClientInitializationErrorDatabase connection failedRetry with backoff, alert ops if persistent

update adds P2025 (record not found) to this list. delete adds both P2025 and P2003/P2014 (cannot delete because dependent records exist).

Here is the dangerous version that most tutorials show:

// Bare call with no error handling - crashes your server on duplicate email
const user = await prisma.user.create({
  data: { email: req.body.email, name: req.body.name },
});

This is the code that wakes you up at 3am. A user signs up with an email that already exists, Prisma throws P2002, and your Express/Next.js app returns a 500 Internal Server Error instead of a helpful "Email already taken" message.


Prisma Error Codes Reference Table

Here are the error codes you will encounter most often in a typical CRUD application:

CodeNameTriggerCommon Fix
P2002Unique constraint violationInsert/update creates duplicate valueCheck for existing record first, or catch and return 409
P2025Record not foundupdate/delete/findOrThrow on missing recordVerify record exists, or catch and return 404
P2003Foreign key constraintInsert references non-existent parent recordCreate parent first, or validate ID exists
P2014Required relation violationDelete a record that other records depend onDelete dependents first, or use cascade delete
P2021Table does not existQuery references a table not in the databaseRun prisma migrate deploy or check schema
P2024Transaction timeoutTransaction took too longBreak into smaller transactions, increase timeout
P2034Transaction deadlockConcurrent transactions waiting on each otherRetry with exponential backoff and jitter

All of these codes are properties of PrismaClientKnownRequestError. You can always access error.code to get the string, and error.meta for additional context like which fields or tables are involved.


How to Handle Prisma Connection Errors vs Query Errors?

Connection errors and query errors are fundamentally different problems that need different recovery strategies.

Connection errors throw PrismaClientInitializationError. They mean your application cannot reach the database at all. Common causes: database server is down, connection string is wrong, network partition, SSL certificate mismatch, connection pool exhausted.

Query errors throw PrismaClientKnownRequestError with a specific code. They mean the database received your query and rejected it for a business logic reason (duplicate, not found, constraint violation).

The recovery strategy differs:

import {
  PrismaClientKnownRequestError,
  PrismaClientInitializationError,
} from '@prisma/client/runtime/library';

async function createUserWithRetry(
  email: string,
  name: string,
  maxRetries = 3
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await prisma.user.create({
        data: { email, name },
      });
    } catch (error) {
      // Connection errors: retry with backoff
      if (error instanceof PrismaClientInitializationError) {
        if (attempt === maxRetries) {
          throw new Error('Database unavailable after retries');
        }
        const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }

      // Query errors: do NOT retry, they will fail again
      if (error instanceof PrismaClientKnownRequestError) {
        if (error.code === 'P2002') {
          throw new Error('Email already exists');
        }
        throw error;
      }

      throw error;
    }
  }
}

The key insight: retrying a P2002 is pointless. The duplicate still exists. But retrying a connection error makes sense because the database may come back.

Prisma Client connects to the database automatically on the first query. If you need to verify the connection at startup (for health checks or fail-fast behavior), call prisma.$connect() explicitly:

async function startServer() {
  try {
    await prisma.$connect();
    console.log('Database connected');
  } catch (error) {
    if (error instanceof PrismaClientInitializationError) {
      console.error('Failed to connect to database:', error.message);
      process.exit(1);
    }
    throw error;
  }

  app.listen(3000);
}

Generic Catch vs Specific Error Code Matching

A generic catch block handles the immediate crash but loses all the information Prisma gives you:

// Generic catch - works but wastes Prisma's error detail
try {
  await prisma.user.create({ data: { email, name } });
} catch (error) {
  console.error('Something went wrong:', error);
  res.status(500).json({ error: 'Internal server error' });
}

This turns every Prisma error into a 500. Your user sees "Internal server error" whether they typed a duplicate email or your database is on fire. Your error logs show the full error, but your API response is useless.

Specific error code matching gives you proper HTTP status codes and user-facing messages:

// Specific matching - proper status codes, actionable messages
try {
  const user = await prisma.user.create({ data: { email, name } });
  res.status(201).json(user);
} catch (error) {
  if (error instanceof PrismaClientKnownRequestError) {
    switch (error.code) {
      case 'P2002': {
        const fields = (error.meta?.target as string[]) ?? ['field'];
        res.status(409).json({
          error: 'CONFLICT',
          message: `A record with that ${fields.join(', ')} already exists.`,
        });
        return;
      }
      case 'P2003':
        res.status(400).json({
          error: 'BAD_REQUEST',
          message: 'Referenced record does not exist.',
        });
        return;
    }
  }
  if (error instanceof PrismaClientInitializationError) {
    res.status(503).json({
      error: 'SERVICE_UNAVAILABLE',
      message: 'Database is temporarily unavailable.',
    });
    return;
  }
  // Genuinely unexpected error
  console.error('Unhandled Prisma error:', error);
  res.status(500).json({ error: 'INTERNAL_ERROR' });
}

The pattern: catch PrismaClientKnownRequestError first, switch on .code, and fall through to a generic 500 only for errors you genuinely did not expect.


How to Handle Prisma Transaction Errors (P2034 Deadlock, P2024 Timeout)?

Transactions add another layer. When any operation inside prisma.$transaction() fails, the entire transaction rolls back and the error propagates.

async function transferCredits(fromId: string, toId: string, amount: number) {
  try {
    const result = await prisma.$transaction(async (tx) => {
      const sender = await tx.user.update({
        where: { id: fromId },
        data: { credits: { decrement: amount } },
      });

      if (sender.credits < 0) {
        throw new Error('Insufficient credits');
      }

      const receiver = await tx.user.update({
        where: { id: toId },
        data: { credits: { increment: amount } },
      });

      return { sender, receiver };
    });

    return result;
  } catch (error) {
    if (error instanceof PrismaClientKnownRequestError) {
      if (error.code === 'P2025') {
        throw new Error('One or both users not found');
      }
      if (error.code === 'P2034') {
        // Deadlock - safe to retry
        throw new RetryableError('Transaction deadlock, please retry');
      }
    }
    throw error;
  }
}

According to the Prisma documentation, P2034 (deadlock) and P2024 (timeout) are the transaction-specific error codes to watch for. Deadlocks are safe to retry with exponential backoff and jitter. Timeouts mean your transaction is too large or the database is under heavy load.


Automating Prisma Error Handling Checks

Manually auditing every prisma.* call in a large codebase is tedious. A project with 50 Prisma calls needs 50 try-catch blocks, each matching the right error codes for that operation type.

Tools like Nark automate this by scanning your TypeScript AST for Prisma calls that lack proper error handling. Nark's @prisma/client profile covers create, update, delete, findUniqueOrThrow, upsert, $transaction, $connect, and $disconnect, each with the specific error codes that operation can throw.

npx nark --tsconfig ./tsconfig.json

Nark flags two patterns:

  1. Bare Prisma calls with no try-catch at all (the 3am crash risk)
  2. Generic catch blocks that do not check error.code (the "everything is a 500" problem)

The output tells you which file, which line, and which specific postcondition is unhandled.


Frequently Asked Questions

Does prisma.findUnique() throw on missing records?

No. findUnique returns null when no record matches. If you want it to throw, use findUniqueOrThrow, which throws PrismaClientKnownRequestError with code P2025.

Can I catch Prisma errors with instanceof checks?

Yes, but you must import from the correct path. Import PrismaClientKnownRequestError from @prisma/client/runtime/library, not from @prisma/client. The main @prisma/client export does not re-export the error classes.

Should I retry on P2002 unique constraint errors?

No. A P2002 means a record with that unique value already exists. Retrying will produce the same error. Instead, catch P2002 and either inform the user or query the existing record.

What is the difference between P2003 and P2014?

P2003 is a foreign key constraint failure (you referenced a parent record that does not exist). P2014 is a required relation violation (you tried to delete a record that other records depend on). Both indicate data integrity issues, but P2003 happens on insert/update and P2014 happens on delete.


How to Handle Prisma Errors in Next.js Server Actions?

Next.js Server Actions cannot throw errors to the client. If a Prisma call throws inside a Server Action, Next.js returns an opaque "Internal Server Error" with no detail. You need to catch Prisma errors and return a structured result object instead.

'use server';

import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { revalidatePath } from 'next/cache';

const prisma = new PrismaClient();

type ActionResult = {
  success: boolean;
  error?: string;
  fieldErrors?: Record<string, string>;
};

export async function createUser(formData: FormData): Promise<ActionResult> {
  const email = formData.get('email') as string;
  const name = formData.get('name') as string;

  try {
    await prisma.user.create({
      data: { email, name },
    });

    revalidatePath('/users');
    return { success: true };
  } catch (error) {
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      const fields = (error.meta?.target as string[]) ?? ['field'];
      return {
        success: false,
        fieldErrors: { [fields[0]]: `A user with that ${fields[0]} already exists.` },
      };
    }
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === 'P2003'
    ) {
      return {
        success: false,
        error: 'Referenced record does not exist.',
      };
    }

    // Log unexpected errors server-side, return generic message to client
    console.error('Unhandled Prisma error in Server Action:', error);
    return { success: false, error: 'Something went wrong. Please try again.' };
  }
}

The pattern: never throw from a Server Action. Always return { success, error } so the client component can display the right message. Use revalidatePath only on success. Log unexpected errors server-side so you can debug them without leaking database details to the browser.

This same pattern works for update and delete actions. Add P2025 handling for operations that can fail on missing records.


Prisma Error Handling Best Practices

These are the key patterns that separate production-grade Prisma code from tutorial-quality code:

  1. Always import error classes from @prisma/client/runtime/library, not from @prisma/client. The main export does not re-export error classes.

  2. Match specific error codes, not generic catches. Switch on error.code to return proper HTTP status codes (409 for P2002, 404 for P2025, 503 for connection errors) instead of blanket 500s.

  3. Use error.meta.target to build specific messages. For P2002, error.meta.target tells you exactly which fields violated the constraint. Return "Email already taken" instead of "Duplicate record."

  4. Only retry connection errors, never query errors. A P2002 or P2025 will fail identically on retry. A PrismaClientInitializationError may succeed if the database comes back. Use exponential backoff with jitter for retries.

  5. Handle P2034 (deadlock) and P2024 (timeout) in transactions. These are the only transaction-specific errors worth retrying. Break large transactions into smaller units if P2024 occurs repeatedly.

  6. Use findUnique (returns null) instead of findUniqueOrThrow (throws P2025) when you expect the record might not exist. Reserve OrThrow variants for cases where a missing record is genuinely exceptional.

  7. Validate the connection at startup. Call prisma.$connect() in your server bootstrap and fail fast if the database is unreachable, rather than discovering it on the first user request.

  8. Automate coverage checks. Run npx nark --tsconfig ./tsconfig.json to find every bare Prisma call that lacks error handling before your users find them for you.


Try It Now

npx nark --tsconfig ./tsconfig.json

Nark checks 160+ packages — including axios, prisma, stripe, and redis — for correct error handling, resource cleanup, and profile violations. Its @prisma/client profile covers every CRUD operation, transaction, and connection lifecycle method with the specific error codes documented above. Run it against your project to find every unhandled Prisma call before your users do.