Files
2025-11-29 18:22:25 +08:00

11 KiB

name, description, allowed-tools
name description allowed-tools
handling-transaction-errors Handle transaction errors properly with P-code checking and timeout configuration. Use when implementing transaction error recovery. Read, Write, Edit

Transaction Error Handling

Handle transaction errors properly with P-code checking, timeout configuration, and recovery patterns.

Error Catching in Transactions

All transaction operations must be wrapped in try/catch blocks to handle failures gracefully.

try {
  await prisma.$transaction(async (tx) => {
    const user = await tx.user.create({
      data: { email: 'user@example.com' }
    });

    await tx.profile.create({
      data: { userId: user.id, bio: 'Hello' }
    });
  });
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    console.error(`Transaction failed: ${error.code}`);
  }
  throw error;
}

P-Code Error Handling

P2002: Unique Constraint Violation

import { Prisma } from '@prisma/client';

try {
  await prisma.$transaction(async (tx) => {
    await tx.user.create({
      data: { email: 'duplicate@example.com' }
    });
  });
} catch (error) {
  if (
    error instanceof Prisma.PrismaClientKnownRequestError &&
    error.code === 'P2002'
  ) {
    const target = error.meta?.target as string[];
    throw new Error(`Unique constraint failed on: ${target.join(', ')}`);
  }
  throw error;
}

P2025: Record Not Found

try {
  await prisma.$transaction(async (tx) => {
    const user = await tx.user.update({
      where: { id: nonExistentId },
      data: { name: 'New Name' }
    });
  });
} catch (error) {
  if (
    error instanceof Prisma.PrismaClientKnownRequestError &&
    error.code === 'P2025'
  ) {
    throw new Error('Record to update not found');
  }
  throw error;
}

Comprehensive P-Code Handler

function handlePrismaError(error: unknown): Error {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    switch (error.code) {
      case 'P2002':
        return new Error(
          `Unique constraint violation: ${error.meta?.target}`
        );
      case 'P2025':
        return new Error('Record not found');
      case 'P2034':
        return new Error('Transaction conflict, please retry');
      default:
        return new Error(`Database error: ${error.code}`);
    }
  }
  if (error instanceof Prisma.PrismaClientUnknownRequestError) {
    return new Error('Unknown database error');
  }
  if (error instanceof Prisma.PrismaClientValidationError) {
    return new Error('Invalid query parameters');
  }
  return error instanceof Error ? error : new Error('Unknown error');
}

try {
  await prisma.$transaction(async (tx) => {
    await tx.user.create({ data: { email: 'test@example.com' } });
  });
} catch (error) {
  throw handlePrismaError(error);
}

Timeout Configuration

Basic Timeout Settings

await prisma.$transaction(
  async (tx) => {
    await tx.user.create({ data: { email: 'user@example.com' } });
    await tx.profile.create({ data: { userId: 1, bio: 'Bio' } });
  },
  {
    maxWait: 5000,
    timeout: 10000,
  }
);

Configuration:

  • maxWait: Maximum time (ms) to wait for transaction to start (default: 2000)
  • timeout: Maximum time (ms) for transaction to complete (default: 5000)

Handling Timeout Errors

try {
  await prisma.$transaction(
    async (tx) => {
      await tx.user.findMany();
      await new Promise(resolve => setTimeout(resolve, 15000));
    },
    { timeout: 10000 }
  );
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    if (error.message.includes('timeout')) {
      throw new Error('Transaction timed out, please try again');
    }
  }
  throw error;
}

Long-Running Transactions

await prisma.$transaction(
  async (tx) => {
    const users = await tx.user.findMany();

    for (const user of users) {
      await tx.auditLog.create({
        data: {
          userId: user.id,
          action: 'BATCH_UPDATE',
          timestamp: new Date(),
        }
      });
    }
  },
  {
    maxWait: 10000,
    timeout: 60000,
  }
);

Recovery Patterns

Retry Strategy with Exponential Backoff

async function transactionWithRetry<T>(
  operation: (tx: Prisma.TransactionClient) => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await prisma.$transaction(operation, {
        timeout: 10000,
      });
    } catch (error) {
      lastError = error instanceof Error ? error : new Error('Unknown error');

      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2034'
      ) {
        const delay = Math.pow(2, attempt) * 100;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }

  throw new Error(`Transaction failed after ${maxRetries} retries: ${lastError?.message}`);
}

const result = await transactionWithRetry(async (tx) => {
  return await tx.user.create({
    data: { email: 'user@example.com' }
  });
});

Idempotent Retry Pattern

async function upsertWithRetry(email: string, name: string) {
  try {
    return await prisma.$transaction(async (tx) => {
      return await tx.user.upsert({
        where: { email },
        create: { email, name },
        update: { name },
      });
    });
  } catch (error) {
    if (
      error instanceof Prisma.PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      return await prisma.user.update({
        where: { email },
        data: { name },
      });
    }
    throw error;
  }
}

Graceful Degradation

async function transferFunds(fromId: number, toId: number, amount: number) {
  try {
    return await prisma.$transaction(
      async (tx) => {
        const from = await tx.account.update({
          where: { id: fromId },
          data: { balance: { decrement: amount } },
        });

        if (from.balance < 0) {
          throw new Error('Insufficient funds');
        }

        await tx.account.update({
          where: { id: toId },
          data: { balance: { increment: amount } },
        });

        return { success: true };
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
        timeout: 5000,
      }
    );
  } catch (error) {
    if (error instanceof Error && error.message === 'Insufficient funds') {
      return { success: false, reason: 'insufficient_funds' };
    }

    if (
      error instanceof Prisma.PrismaClientKnownRequestError &&
      error.code === 'P2025'
    ) {
      return { success: false, reason: 'account_not_found' };
    }

    throw error;
  }
}

Compensating Transactions

async function createOrderWithInventory(
  productId: number,
  quantity: number,
  userId: number
) {
  let orderId: number | null = null;

  try {
    const result = await prisma.$transaction(async (tx) => {
      const product = await tx.product.update({
        where: { id: productId },
        data: { stock: { decrement: quantity } },
      });

      if (product.stock < 0) {
        throw new Error('Insufficient stock');
      }

      const order = await tx.order.create({
        data: {
          userId,
          productId,
          quantity,
          status: 'PENDING',
        },
      });

      orderId = order.id;

      return order;
    });

    return result;
  } catch (error) {
    if (orderId) {
      await prisma.order.update({
        where: { id: orderId },
        data: { status: 'FAILED' },
      });
    }

    throw error;
  }
}

Isolation Level Error Handling

try {
  await prisma.$transaction(
    async (tx) => {
      const balance = await tx.account.findUnique({
        where: { id: accountId },
      });

      await tx.account.update({
        where: { id: accountId },
        data: { balance: balance!.balance + amount },
      });
    },
    {
      isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
    }
  );
} catch (error) {
  if (
    error instanceof Prisma.PrismaClientKnownRequestError &&
    error.code === 'P2034'
  ) {
    throw new Error('Serialization failure, transaction will be retried');
  }
  throw error;
}

Common Patterns

Validation Before Transaction

async function createUserWithProfile(email: string, name: string) {
  const existing = await prisma.user.findUnique({
    where: { email },
  });

  if (existing) {
    throw new Error('User already exists');
  }

  try {
    return await prisma.$transaction(async (tx) => {
      const user = await tx.user.create({
        data: { email, name },
      });

      await tx.profile.create({
        data: { userId: user.id },
      });

      return user;
    });
  } catch (error) {
    if (
      error instanceof Prisma.PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      throw new Error('User was created by another request');
    }
    throw error;
  }
}

Nested Error Context

class TransactionError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'TransactionError';
  }
}

async function complexTransaction(data: unknown) {
  try {
    return await prisma.$transaction(async (tx) => {
      const user = await tx.user.create({
        data: data as Prisma.UserCreateInput,
      });

      return user;
    });
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      throw new TransactionError(
        'Transaction failed',
        error.code,
        { meta: error.meta, data }
      );
    }
    throw error;
  }
}

Anti-Patterns

DON'T: Ignore Error Types

try {
  await prisma.$transaction(async (tx) => {
    await tx.user.create({ data: { email: 'test@example.com' } });
  });
} catch (error) {
  console.error('Error occurred');
}

DO: Handle Specific Error Types

try {
  await prisma.$transaction(async (tx) => {
    await tx.user.create({ data: { email: 'test@example.com' } });
  });
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    console.error(`Database error ${error.code}: ${error.message}`);
  } else {
    console.error('Unexpected error:', error);
  }
  throw error;
}

DON'T: Use Default Timeouts for Long Operations

await prisma.$transaction(async (tx) => {
  for (let i = 0; i < 10000; i++) {
    await tx.log.create({ data: { message: `Log ${i}` } });
  }
});

DO: Configure Appropriate Timeouts

await prisma.$transaction(
  async (tx) => {
    const logs = Array.from({ length: 10000 }, (_, i) => ({
      message: `Log ${i}`,
    }));

    await tx.log.createMany({ data: logs });
  },
  { timeout: 30000 }
);

TypeScript Error Handling:

  • If implementing runtime checks for error codes, use the using-runtime-checks skill from typescript for assertion and guard patterns