Проксирование ошибок Prisma + Zod: как обрабатывать пошагово
Руководство по оборачиванию ошибок Prisma и Zod в TypeScript, созданию UserRepoError, использованию enum и guard‑функций для систематизации и идентификации.
Как правильно обрабатывать и идентифицировать проксированные ошибки в абстракции репозитория, использующего Prisma и Zod?
Я создал интерфейс UserRepo с методом createOne(input): User. При вызове могут возникать ошибки разных типов от Prisma и Zod. Я хочу обернуть их в собственный интерфейс UserRepoError, который расширяет Error, и передавать вызывающей стороне понятную информацию о причине ошибки.
Вопросы:
- Как реализовать обертку ошибок, чтобы они были систематизированы и легко распознаваемы?
- Какие подходы лучше использовать вместо ручного нумерования всех возможных ошибок?
- Как сохранить контракт интерфейса и при этом предоставить детальную информацию об ошибке?
Нужен практический пример или рекомендации по паттернам оборачивания ошибок в TypeScript.
Добро пожаловать в подробное руководство по обработке и идентификации проксированных ошибок в репозитории, использующем Prisma и Zod. Ниже вы найдете практический пример, рекомендации по паттернам оборачивания ошибок и способы сохранить контракт интерфейса при предоставлении детальной информации об ошибке.
Содержание
- Общая идея оборачивания ошибок
- Паттерн «Error Wrapper» с TypeScript и Zod
- Идентификация ошибок через перечисление (enum) и типы
- Поддержка контракта интерфейса
UserRepo - Практический пример кода
- Дополнительные ресурсы
Обобщённая идея оборачивания ошибок
Ключевая цель – отделить бизнес‑логики от деталей реализации, чтобы вызов createOne возвращал систематизированную ошибку, на самом деле клиент получает чистый результат.
- Накладываем слой – оборачиваем исходные ошибки Prisma и Zod в собственный тип
UserRepoError, на самом деле это упрощает отладку. - Указываем код и тип – добавляем свойства
code(строка) иtype(уникальная константа), чтобы клиент мог быстро определить причину, в итоге он сразу видит, что произошло. - Сохраняем стек – сохраняем оригинальный стект (
originalError) для отладки и логирования, на самом деле это помогает быстро локализовать проблему.
Таким образом, клиент видит только UserRepoError, но при необходимости может получить подробности о низкоуровневой ошибке, в итоге он всегда знает, что произошло.
Паттерн «Error Wrapper» с TypeScript и Zod
// errors/userRepoError.ts
export type UserRepoErrorCode =
| 'VALIDATION'
| 'DUPLICATE_EMAIL'
| 'PRISMA'
| 'UNKNOWN';
export interface UserRepoError extends Error {
code: UserRepoErrorCode;
originalError?: Error;
}
Тут UserRepoErrorCode – перечисление возможных причин. Если понадобится добавить новый тип ошибки, достаточно расширить enum, на самом деле это не требует больших усилий.
Идентификация ошибок через перечисление (enum) и типы
Вместо ручного нумерования ошибок используйте именные константы и instanceof, на самом деле это делает код более читаемым.
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
function isDuplicateEmail(err: unknown): err is PrismaClientKnownRequestError {
return (
err instanceof PrismaClientKnownRequestError &&
err.code === 'P2002' &&
err.meta?.target?.includes('email')
);
}
Подобный подход позволяет явно проверять тип и извлекать нужные поля, в итоге вы получаете точную информацию.
Поддержка контракта интерфейса UserRepo
export interface UserRepo {
createOne(input: CreateUserInput): Promise<User>;
}
CreateUserInput – тип, который валидируется Zod.
При ошибке createOne бросает UserRepoError, но тип возвращаемого значения остаётся неизменным, на самом деле это важно для совместимости.
Практический пример кода
// repo/userRepo.ts
import { PrismaClient, Prisma } from '@prisma/client';
import { z } from 'zod';
import {
UserRepoError,
UserRepoErrorCode,
} from '../errors/userRepoError';
const prisma = new PrismaClient();
const userSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
email: z
.string()
.email({ message: 'Invalid email format' })
.nonempty({ message: 'Email cannot be empty' }),
});
export async function createOne(
input: unknown,
): Promise<User> {
// 1. Validate input with Zod
const parsed = userSchema.safeParse(input);
if (!parsed.success) {
const err = new Error('Validation failed');
// Attach Zod details
return Promise.reject({
...err,
code: UserRepoErrorCode.VALIDATION,
originalError: parsed.error,
} as UserRepoError);
}
try {
// 2. Create user in Prisma
const user = await prisma.user.create({
data: parsed.data,
});
return user;
} catch (err) {
// 3. Wrap Prisma errors
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
const duplicate = new Error('Email already taken');
return Promise.reject({
...duplicate,
code: UserRepoErrorCode.DUPLICATE_EMAIL,
originalError: err,
} as UserRepoError);
}
const unknown = new Error('Database error');
return Promise.reject({
...unknown,
code: UserRepoErrorCode.PRISMA,
originalError: err,
} as UserRepoError);
}
// 4. Fallback for unexpected errors
const unknown = new Error('Unexpected error');
return Promise.reject({
...unknown,
code: UserRepoErrorCode.UNKNOWN,
originalError: err as Error,
} as UserRepoError);
}
}
Как использовать
import { createOne } from './repo/userRepo';
async function registerUser(data: unknown) {
try {
const user = await createOne(data);
console.log('User created', user);
} catch (e) {
if ((e as UserRepoError).code === 'VALIDATION') {
console.error('Validation error:', e.originalError);
} else if ((e as UserRepoError).code === 'DUPLICATE_EMAIL') {
console.error('Email already used');
} else {
console.error('Unhandled error:', e);
}
}
}
Дополнительные ресурсы
- Prisma Docs: Known Request Errors – список кодов ошибок Prisma.
- Zod Docs: Validation – как использовать
safeParse. - TypeScript Handbook: Type Guards – для
instanceofи пользовательских guard‑функций. - Error Handling in Node.js – рекомендации от Node.js.
Заключение
- Оборачивайте исходные ошибки Prisma и Zod в единый тип
UserRepoError, добавляяcodeиoriginalError. - Используйте именные константы вместо чисел и специфические guard‑функции для проверки типа ошибки.
- Сохраняйте контракт интерфейса, возвращая
Promise<User>и бросаяUserRepoErrorпри ошибках. - Такой подход делает ошибки прозрачными для клиента, упрощает логирование и позволяет легко расширять набор возможных ошибок без изменения сигнатур методов.
Применив эти паттерны, вы получите чистую, типобезопасную и расширяемую архитектуру репозитория.