510 lines
11 KiB
Markdown
510 lines
11 KiB
Markdown
---
|
|
name: handling-transaction-errors
|
|
description: Handle transaction errors properly with P-code checking and timeout configuration. Use when implementing transaction error recovery.
|
|
allowed-tools: 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.
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
await prisma.$transaction(async (tx) => {
|
|
for (let i = 0; i < 10000; i++) {
|
|
await tx.log.create({ data: { message: `Log ${i}` } });
|
|
}
|
|
});
|
|
```
|
|
|
|
### DO: Configure Appropriate Timeouts
|
|
|
|
```typescript
|
|
await prisma.$transaction(
|
|
async (tx) => {
|
|
const logs = Array.from({ length: 10000 }, (_, i) => ({
|
|
message: `Log ${i}`,
|
|
}));
|
|
|
|
await tx.log.createMany({ data: logs });
|
|
},
|
|
{ timeout: 30000 }
|
|
);
|
|
```
|
|
|
|
## Related Skills
|
|
|
|
**TypeScript Error Handling:**
|
|
|
|
- If implementing runtime checks for error codes, use the using-runtime-checks skill from typescript for assertion and guard patterns
|