Prisma Integration
Turn Prisma database errors into typed Results for exhaustive error handling.
Why Combine Them?
Section titled “Why Combine Them?”- Exhaustive error handling — Handle
NOT_FOUND,UNIQUE_VIOLATION, etc. explicitly - No more try/catch spaghetti — Database operations compose cleanly in workflows
- Type-safe error codes — Prisma’s error codes become typed union members
Quick Start
Section titled “Quick Start”import { Prisma } from '@prisma/client';import { ok, err, type AsyncResult } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
type DbError = | { type: 'NOT_FOUND' } | { type: 'UNIQUE_VIOLATION'; field: string } | { type: 'DB_ERROR'; message: string };
const findUser = async (id: string): AsyncResult<User, DbError> => { try { const user = await prisma.user.findUniqueOrThrow({ where: { id } }); return ok(user); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { if (e.code === 'P2025') return err({ type: 'NOT_FOUND' }); } return err({ type: 'DB_ERROR', message: String(e) }); }};
// Use in a workflowconst workflow = createWorkflow('workflow', { findUser });
const result = await workflow.run(async ({ step, deps }) => { const user = await step('findUser', () => deps.findUser('user-123')); return user;});Patterns
Section titled “Patterns”Pattern 1: Wrapping Common Prisma Operations
Section titled “Pattern 1: Wrapping Common Prisma Operations”Create a generic wrapper for Prisma operations:
import { Prisma, PrismaClient } from '@prisma/client';import { ok, err, type AsyncResult } from 'awaitly';
const prisma = new PrismaClient();
// Common database error typestype DbError = | { type: 'NOT_FOUND'; entity?: string } | { type: 'UNIQUE_VIOLATION'; field: string } | { type: 'FOREIGN_KEY_VIOLATION'; field: string } | { type: 'DB_ERROR'; code?: string; message: string };
// Generic wrapperconst prismaToResult = async <T>( operation: () => Promise<T>, entity?: string): AsyncResult<T, DbError> => { try { return ok(await operation()); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { switch (e.code) { case 'P2025': // Record not found return err({ type: 'NOT_FOUND', entity }); case 'P2002': // Unique constraint violation return err({ type: 'UNIQUE_VIOLATION', field: (e.meta?.target as string[])?.join(', ') ?? 'unknown' }); case 'P2003': // Foreign key constraint violation return err({ type: 'FOREIGN_KEY_VIOLATION', field: (e.meta?.field_name as string) ?? 'unknown' }); default: return err({ type: 'DB_ERROR', code: e.code, message: e.message }); } } return err({ type: 'DB_ERROR', message: String(e) }); }};
// Usageconst user = await prismaToResult( () => prisma.user.findUniqueOrThrow({ where: { id } }), 'User');Pattern 2: Typed Repository Functions
Section titled “Pattern 2: Typed Repository Functions”Create repository functions with explicit error types:
import { ok, err, type AsyncResult } from 'awaitly';
type UserNotFoundError = { type: 'USER_NOT_FOUND'; id: string };type EmailTakenError = { type: 'EMAIL_TAKEN'; email: string };type DbError = { type: 'DB_ERROR'; message: string };
// Repository with typed errorsconst userRepository = { findById: async (id: string): AsyncResult<User, UserNotFoundError | DbError> => { try { const user = await prisma.user.findUnique({ where: { id } }); if (!user) return err({ type: 'USER_NOT_FOUND', id }); return ok(user); } catch (e) { return err({ type: 'DB_ERROR', message: String(e) }); } },
create: async (data: { email: string; name: string }): AsyncResult<User, EmailTakenError | DbError> => { try { const user = await prisma.user.create({ data }); return ok(user); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { return err({ type: 'EMAIL_TAKEN', email: data.email }); } return err({ type: 'DB_ERROR', message: String(e) }); } },
update: async (id: string, data: Partial<User>): AsyncResult<User, UserNotFoundError | DbError> => { try { const user = await prisma.user.update({ where: { id }, data }); return ok(user); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') { return err({ type: 'USER_NOT_FOUND', id }); } return err({ type: 'DB_ERROR', message: String(e) }); } },
delete: async (id: string): AsyncResult<void, UserNotFoundError | DbError> => { try { await prisma.user.delete({ where: { id } }); return ok(undefined); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') { return err({ type: 'USER_NOT_FOUND', id }); } return err({ type: 'DB_ERROR', message: String(e) }); } },};Pattern 3: In Workflows with Validation
Section titled “Pattern 3: In Workflows with Validation”Combine Prisma with Zod validation:
import { createWorkflow } from 'awaitly/workflow';import { z } from 'zod';
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(2).max(100), role: z.enum(['user', 'admin']).default('user'),});
const zodToResult = <T>(schema: z.ZodSchema<T>, data: unknown) => { const parsed = schema.safeParse(data); return parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION' as const, issues: parsed.error.issues });};
const workflow = createWorkflow('workflow', { validateInput: (raw: unknown) => zodToResult(CreateUserSchema, raw), createUser: userRepository.create,});
const createUser = async (rawInput: unknown) => { return workflow.run(async ({ step, deps }) => { const input = await step('validateInput', () => deps.validateInput(rawInput)); const user = await step('createUser', () => deps.createUser(input)); return user; });};
// Error type is automatically inferred:// ValidationError | EmailTakenError | DbError | UnexpectedErrorPattern 4: Transactions with Saga Pattern
Section titled “Pattern 4: Transactions with Saga Pattern”Use awaitly’s saga pattern for transactions that need compensation:
import { createSagaWorkflow } from 'awaitly/saga';
const transferFunds = createSagaWorkflow('saga', { debitAccount: async (accountId: string, amount: number) => { return prismaToResult(() => prisma.account.update({ where: { id: accountId }, data: { balance: { decrement: amount } }, }) ); }, creditAccount: async (accountId: string, amount: number) => { return prismaToResult(() => prisma.account.update({ where: { id: accountId }, data: { balance: { increment: amount } }, }) ); }, createTransaction: async (data: TransactionData) => { return prismaToResult(() => prisma.transaction.create({ data })); },});
const result = await transferFunds(async (ctx, deps) => { // Debit source account await ctx.step( 'debit', () => deps.debitAccount(sourceId, amount), { compensate: () => deps.creditAccount(sourceId, amount) } // Rollback on failure );
// Credit destination account await ctx.step( 'credit', () => deps.creditAccount(destId, amount), { compensate: () => deps.debitAccount(destId, amount) } );
// Record transaction await ctx.step('createTransaction', () => deps.createTransaction({ sourceId, destId, amount, timestamp: new Date(), }));
return { success: true };});Pattern 5: Handling Specific Error Codes
Section titled “Pattern 5: Handling Specific Error Codes”Map Prisma error codes to business errors:
import { Prisma } from '@prisma/client';import { err, type AsyncResult } from 'awaitly';
// Prisma error codes reference:// P2000 - Value too long// P2002 - Unique constraint violation// P2003 - Foreign key constraint violation// P2025 - Record not found// See: https://www.prisma.io/docs/reference/api-reference/error-reference
type OrderError = | { type: 'ORDER_NOT_FOUND'; orderId: string } | { type: 'PRODUCT_NOT_FOUND'; productId: string } | { type: 'DUPLICATE_ORDER'; orderNumber: string } | { type: 'DB_ERROR'; message: string };
const mapPrismaError = ( e: unknown, context: { orderId?: string; productId?: string; orderNumber?: string }): OrderError => { if (e instanceof Prisma.PrismaClientKnownRequestError) { switch (e.code) { case 'P2025': if (context.orderId) return { type: 'ORDER_NOT_FOUND', orderId: context.orderId }; if (context.productId) return { type: 'PRODUCT_NOT_FOUND', productId: context.productId }; return { type: 'DB_ERROR', message: 'Record not found' };
case 'P2002': if (context.orderNumber) return { type: 'DUPLICATE_ORDER', orderNumber: context.orderNumber }; return { type: 'DB_ERROR', message: 'Duplicate record' };
default: return { type: 'DB_ERROR', message: e.message }; } } return { type: 'DB_ERROR', message: String(e) };};Complete Example: User Signup
Section titled “Complete Example: User Signup”Complete workflow combining Zod validation, email checking, and user creation:
import { z } from 'zod';import { Prisma, PrismaClient } from '@prisma/client';import { ok, err, type AsyncResult } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
const prisma = new PrismaClient();
// Schemasconst SignUpSchema = z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(2).max(100),});
// Error typestype ValidationError = { type: 'VALIDATION'; issues: z.ZodIssue[] };type EmailTakenError = { type: 'EMAIL_TAKEN'; email: string };type DbError = { type: 'DB_ERROR'; message: string };
type SignUpError = ValidationError | EmailTakenError | DbError;
// Dependenciesconst deps = { validateInput: (raw: unknown) => { const parsed = SignUpSchema.safeParse(raw); return Promise.resolve( parsed.success ? ok(parsed.data) : err({ type: 'VALIDATION' as const, issues: parsed.error.issues }) ); },
hashPassword: async (password: string) => { // In real code, use bcrypt or argon2 return ok(`hashed_${password}`); },
createUser: async (data: { email: string; passwordHash: string; name: string }): AsyncResult< { id: string; email: string; name: string }, EmailTakenError | DbError > => { try { const user = await prisma.user.create({ data, select: { id: true, email: true, name: true }, }); return ok(user); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { return err({ type: 'EMAIL_TAKEN', email: data.email }); } return err({ type: 'DB_ERROR', message: String(e) }); } },};
// Sign up workflowconst signUpWorkflow = createWorkflow('workflow', deps);
const signUp = async (rawInput: unknown) => { return signUpWorkflow.run(async ({ step, deps }) => { const input = await step('validateInput', () => deps.validateInput(rawInput)); const passwordHash = await step('hashPassword', () => deps.hashPassword(input.password)); const user = await step('createUser', () => deps.createUser({ email: input.email, passwordHash, name: input.name, })); return user; });};
// API handlerexport const POST = async (request: Request) => { const result = await signUp(await request.json());
if (!result.ok) { switch (result.error.type) { case 'VALIDATION': return Response.json( { error: 'Validation failed', issues: result.error.issues }, { status: 400 } ); case 'EMAIL_TAKEN': return Response.json( { error: 'Email already registered' }, { status: 409 } ); case 'DB_ERROR': console.error('Database error:', result.error.message); return Response.json( { error: 'Server error' }, { status: 500 } ); } }
return Response.json(result.value, { status: 201 });};Common Utilities
Section titled “Common Utilities”Copy this utility file to your project:
import { Prisma } from '@prisma/client';import { ok, err, type AsyncResult } from 'awaitly';
export type DbError = | { type: 'NOT_FOUND'; entity?: string } | { type: 'UNIQUE_VIOLATION'; field: string } | { type: 'FOREIGN_KEY_VIOLATION'; field: string } | { type: 'DB_ERROR'; code?: string; message: string };
/** * Wrap a Prisma operation and convert errors to typed Results */export const prismaToResult = async <T>( operation: () => Promise<T>, entity?: string): AsyncResult<T, DbError> => { try { return ok(await operation()); } catch (e) { return err(mapPrismaError(e, entity)); }};
/** * Map Prisma errors to typed DbError */export const mapPrismaError = (e: unknown, entity?: string): DbError => { if (e instanceof Prisma.PrismaClientKnownRequestError) { switch (e.code) { case 'P2025': return { type: 'NOT_FOUND', entity }; case 'P2002': return { type: 'UNIQUE_VIOLATION', field: (e.meta?.target as string[])?.join(', ') ?? 'unknown' }; case 'P2003': return { type: 'FOREIGN_KEY_VIOLATION', field: (e.meta?.field_name as string) ?? 'unknown' }; default: return { type: 'DB_ERROR', code: e.code, message: e.message }; } } return { type: 'DB_ERROR', message: String(e) };};
/** * Check if an error is a specific Prisma error code */export const isPrismaError = (e: unknown, code: string): boolean => { return e instanceof Prisma.PrismaClientKnownRequestError && e.code === code;};
/** * Wrap findUnique to return Result with NOT_FOUND error */export const findOrNotFound = async <T>( operation: () => Promise<T | null>, entity: string): AsyncResult<T, { type: 'NOT_FOUND'; entity: string }> => { const result = await operation(); return result !== null ? ok(result) : err({ type: 'NOT_FOUND', entity });};Next Steps
Section titled “Next Steps”- Drizzle Integration for an alternative ORM
- Zod Integration for input validation
- Workflows for composing database operations