Программирование

Проксирование ошибок Prisma + Zod: как обрабатывать пошагово

Руководство по оборачиванию ошибок Prisma и Zod в TypeScript, созданию UserRepoError, использованию enum и guard‑функций для систематизации и идентификации.

Как правильно обрабатывать и идентифицировать проксированные ошибки в абстракции репозитория, использующего Prisma и Zod?

Я создал интерфейс UserRepo с методом createOne(input): User. При вызове могут возникать ошибки разных типов от Prisma и Zod. Я хочу обернуть их в собственный интерфейс UserRepoError, который расширяет Error, и передавать вызывающей стороне понятную информацию о причине ошибки.

Вопросы:

  1. Как реализовать обертку ошибок, чтобы они были систематизированы и легко распознаваемы?
  2. Какие подходы лучше использовать вместо ручного нумерования всех возможных ошибок?
  3. Как сохранить контракт интерфейса и при этом предоставить детальную информацию об ошибке?

Нужен практический пример или рекомендации по паттернам оборачивания ошибок в TypeScript.

Добро пожаловать в подробное руководство по обработке и идентификации проксированных ошибок в репозитории, использующем Prisma и Zod. Ниже вы найдете практический пример, рекомендации по паттернам оборачивания ошибок и способы сохранить контракт интерфейса при предоставлении детальной информации об ошибке.

Содержание

Обобщённая идея оборачивания ошибок

Ключевая цель – отделить бизнес‑логики от деталей реализации, чтобы вызов createOne возвращал систематизированную ошибку, на самом деле клиент получает чистый результат.

  1. Накладываем слой – оборачиваем исходные ошибки Prisma и Zod в собственный тип UserRepoError, на самом деле это упрощает отладку.
  2. Указываем код и тип – добавляем свойства code (строка) и type (уникальная константа), чтобы клиент мог быстро определить причину, в итоге он сразу видит, что произошло.
  3. Сохраняем стек – сохраняем оригинальный стект (originalError) для отладки и логирования, на самом деле это помогает быстро локализовать проблему.

Таким образом, клиент видит только UserRepoError, но при необходимости может получить подробности о низкоуровневой ошибке, в итоге он всегда знает, что произошло.

Паттерн «Error Wrapper» с TypeScript и Zod

ts
// errors/userRepoError.ts
export type UserRepoErrorCode =
  | 'VALIDATION'
  | 'DUPLICATE_EMAIL'
  | 'PRISMA'
  | 'UNKNOWN';

export interface UserRepoError extends Error {
  code: UserRepoErrorCode;
  originalError?: Error;
}

Тут UserRepoErrorCode – перечисление возможных причин. Если понадобится добавить новый тип ошибки, достаточно расширить enum, на самом деле это не требует больших усилий.

Идентификация ошибок через перечисление (enum) и типы

Вместо ручного нумерования ошибок используйте именные константы и instanceof, на самом деле это делает код более читаемым.

ts
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

ts
export interface UserRepo {
  createOne(input: CreateUserInput): Promise<User>;
}

CreateUserInput – тип, который валидируется Zod.
При ошибке createOne бросает UserRepoError, но тип возвращаемого значения остаётся неизменным, на самом деле это важно для совместимости.

Практический пример кода

ts
// 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);
  }
}

Как использовать

ts
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 и Zod в единый тип UserRepoError, добавляя code и originalError.
  • Используйте именные константы вместо чисел и специфические guard‑функции для проверки типа ошибки.
  • Сохраняйте контракт интерфейса, возвращая Promise<User> и бросая UserRepoError при ошибках.
  • Такой подход делает ошибки прозрачными для клиента, упрощает логирование и позволяет легко расширять набор возможных ошибок без изменения сигнатур методов.

Применив эти паттерны, вы получите чистую, типобезопасную и расширяемую архитектуру репозитория.

Авторы
Проверено модерацией
Модерация