Обработка исключений валидации

Библиотека class-validator не выбрасывает исключения автоматически при каждой валидации. Основной результат работы — это массив ошибок типа ValidationError, возвращаемый функциями validate или validateSync. Исключения появляются только в случаях, когда используется validateOrReject, либо когда ошибки преобразуются вручную в исключения приложения (например, в рамках NestJS или собственного слоя обработки).

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


Структура ValidationError и её значение для обработки

Каждая ошибка валидации представлена объектом ValidationError, содержащим подробную информацию о несоответствии данных правилам:

  • property — имя поля, где обнаружена ошибка
  • value — значение, вызвавшее нарушение
  • constraints — объект с текстами нарушенных правил
  • children — вложенные ошибки для сложных объектов

Особенность constraints заключается в том, что одно поле может нарушать сразу несколько правил, например IsString, MinLength, IsNotEmpty, что приводит к множественным сообщениям внутри одного узла ошибки.

Вложенность children критична при работе с DTO, содержащими сложные структуры. Ошибки формируются рекурсивно, сохраняя контекст вложенности объектов.


Базовый механизм перехвата ошибок через validate

Функция validate возвращает массив ошибок вместо исключения, поэтому обработка всегда строится через проверку результата:

const errors = await validate(dto);

if (errors.length > 0) {
  // обработка ошибок
}

Такой подход делает обработку явной, но требует дополнительного кода для преобразования ошибок в формат, пригодный для API или логирования.

Типичная проблема возникает при необходимости унификации ответа: массив ValidationError не подходит для прямой передачи клиенту, поскольку содержит избыточную вложенность и служебные поля.


Преобразование ошибок в читаемый формат

Практика обработки почти всегда включает нормализацию структуры ошибок в плоский формат.

Рекурсивный разбор ValidationError позволяет получить список сообщений:

function flattenErrors(errors: ValidationError[]): string[] {
  const result: string[] = [];

  for (const error of errors) {
    if (error.constraints) {
      result.push(...Object.values(error.constraints));
    }

    if (error.children && error.children.length > 0) {
      result.push(...flattenErrors(error.children));
    }
  }

  return result;
}

Такой подход устраняет вложенность и подготавливает данные для передачи в HTTP-ответ.


Использование validateOrReject и исключений Promise

Функция validateOrReject переводит модель работы в исключительный режим. При наличии ошибок возвращается отклонённый Promise с массивом ValidationError.

await validateOrReject(dto);

При ошибке управление переходит в catch:

try {
  await validateOrReject(dto);
} catch (errors) {
  // обработка ValidationError[]
}

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


Преобразование ошибок в HTTP-исключения

В веб-приложениях часто требуется преобразовать ошибки валидации в HTTP-ошибки. Наиболее распространённый вариант — формирование BadRequestException.

import { BadRequestException } from '@nestjs/common';

try {
  await validateOrReject(dto);
} catch (errors) {
  throw new BadRequestException(errors);
}

Однако прямой возврат ValidationError[] редко используется без трансформации, поскольку формат слишком детализирован для клиента.


Централизованная обработка через exception factory

В системах, использующих NestJS, применяется механизм exceptionFactory, позволяющий контролировать формат ошибки на уровне пайпа валидации.

new ValidationPipe({
  exceptionFactory: (errors) => {
    return new BadRequestException(
      errors.map(e => ({
        field: e.property,
        errors: Object.values(e.constraints || {})
      }))
    );
  }
});

Этот подход переносит ответственность за форматирование ошибок в единый слой, исключая дублирование логики в контроллерах и сервисах.


Обработка вложенных объектов и сложных DTO

При работе со сложными структурами ключевую роль играет корректная обработка children. Ошибки вложенных объектов не поднимаются автоматически в верхний уровень.

Пример проблемы:

  • объект содержит массив DTO
  • внутри каждого элемента возникает ошибка
  • без рекурсивной обработки теряется контекст элемента массива

Решение заключается в сохранении пути к полю при обходе дерева ошибок:

function collectErrors(errors: ValidationError[], path = '') {
  const result: string[] = [];

  for (const error of errors) {
    const currentPath = path ? `${path}.${error.property}` : error.property;

    if (error.constraints) {
      result.push(
        ...Object.values(error.constraints).map(msg => `${currentPath}: ${msg}`)
      );
    }

    if (error.children?.length) {
      result.push(...collectErrors(error.children, currentPath));
    }
  }

  return result;
}

Так формируется полный контекст ошибки, необходимый для диагностики сложных DTO.


Синхронная и асинхронная обработка ошибок

Синхронная функция validateSync возвращает массив ошибок напрямую, без Promise. Это упрощает обработку в утилитарных сценариях, но лишает возможности работы с асинхронными валидаторами.

const errors = validateSync(dto);

Асинхронный вариант validate необходим при использовании кастомных декораторов, работающих с внешними сервисами или базой данных.

Различие влияет на стратегию обработки: синхронный путь чаще используется в утилитах и тестах, асинхронный — в приложениях с внешними зависимостями.


Типизация ошибок и безопасная работа с результатами

ValidationError содержит опциональные поля, что требует аккуратной обработки в TypeScript.

Наиболее уязвимые места:

  • constraints может быть undefined
  • children может отсутствовать
  • value может содержать сложные типы данных

Поэтому безопасная обработка всегда включает проверки наличия полей перед доступом.


Ошибки как часть доменной логики

В некоторых архитектурах ошибки валидации рассматриваются не как технические сбои, а как часть бизнес-правил. В таком случае результат class-validator преобразуется в доменные объекты ошибок.

Например:

  • поле не соответствует формату
  • создаётся доменная ошибка FieldValidationError
  • она агрегируется в DomainValidationException

Такой подход отделяет библиотечную структуру ValidationError от внутренней модели приложения, снижая связанность слоёв.


Логирование и трассировка ошибок валидации

При сложных системах ошибки часто сохраняются в логах. Важно учитывать, что ValidationError может содержать вложенные структуры значительного объёма.

Практика логирования обычно включает:

  • нормализацию ошибок
  • удаление избыточных полей (value, target)
  • сохранение только пути и сообщений

Это снижает нагрузку на систему логирования и упрощает анализ инцидентов.


Обработка ошибок в связке с class-transformer

При использовании class-transformer ошибки могут возникать не только на этапе валидации, но и на этапе преобразования типов. Это создаёт дополнительный слой, где исключения могут появляться до вызова validate.

Типичный сценарий:

  • строка "123" преобразуется в число
  • преобразование не удаётся
  • валидатор получает неконсистентные данные

Поэтому обработка ошибок должна учитывать весь pipeline трансформации и валидации, а не только конечный результат class-validator.