Обработка серверных ошибок

При работе с формами в связке YupResolver и библиотеками управления формами (например, React Hook Form) серверные ошибки представляют отдельный слой валидации, который не покрывается схемой Yup. Клиентская схема отвечает за предварительную проверку данных, тогда как сервер фиксирует бизнес-ограничения, состояние базы данных, конкурентные изменения и правила, которые невозможно или нецелесообразно дублировать на клиенте.

Серверные ошибки условно делятся на несколько категорий:

Ошибки полей (field-level errors) Возникают, когда сервер возвращает структуру, привязанную к конкретным полям формы:

  • email already exists
  • password is too weak
  • username is taken

Глобальные ошибки (form-level errors) Не привязаны к конкретному полю:

  • неверная комбинация данных
  • отсутствие прав доступа
  • логические конфликты между полями

Ошибки структуры запроса

  • неверный формат JSON
  • отсутствие обязательных вложенных объектов
  • несоответствие API контракту

Бизнес-ошибки

  • нарушение ограничений доменной модели
  • ошибки состояния (например, попытка редактирования удалённой сущности)

Разделение ответственности YupResolver и серверной валидации

YupResolver выполняет синхронную или асинхронную проверку схемы до отправки данных на сервер. Его задача — гарантировать соответствие структуры и базовых правил:

  • типизация значений
  • обязательные поля
  • минимальные и максимальные ограничения
  • кастомные проверки Yup

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

Ключевой принцип: YupResolver не обрабатывает серверные ошибки напрямую, но формирует основу для их корректного отображения в форме.


Стандартный поток обработки ошибок

Типичный жизненный цикл данных в форме:

  1. Пользователь вводит данные

  2. YupResolver выполняет проверку схемы

  3. При успехе данные отправляются на сервер

  4. Сервер возвращает:

    • успех (200/201)
    • ошибку (400/422/409)
  5. Ошибка преобразуется в формат формы

  6. Ошибки отображаются через setError


Форматы серверных ошибок и их нормализация

Разные backend-стекы возвращают разные структуры ошибок:

Express / NestJS (пример)

{
  "message": "Validation failed",
  "errors": {
    "email": "Email already exists",
    "password": "Too weak"
  }
}

Laravel-style

{
  "message": "The given data was invalid",
  "errors": {
    "email": ["Email already exists"],
    "password": ["Too weak"]
  }
}

Generic API

{
  "error": "conflict",
  "fields": [
    { "field": "email", "message": "Taken" }
  ]
}

Приведение серверных ошибок к формату формы

Для корректной интеграции требуется слой нормализации:

function normalizeServerErrors(response) {
  const errors = response?.errors;

  if (!errors) return [];

  return Object.entries(errors).map(([field, message]) => ({
    field,
    message: Array.isArray(message) ? message[0] : message
  }));
}

Альтернативный формат массива:

function normalizeFieldsArray(errorsArray) {
  return errorsArray.map(err => ({
    name: err.field,
    type: "server",
    message: err.message
  }));
}

Интеграция с setError

Основной механизм отображения серверных ошибок — метод setError:

import { useForm } from "react-hook-form";

const { setError } = useForm();

async function onSubmit(data) {
  try {
    await api.send(data);
  } catch (err) {
    const normalized = normalizeServerErrors(err.response);

    normalized.forEach(({ field, message }) => {
      setError(field, {
        type: "server",
        message
      });
    });
  }
}

Серверные ошибки не проходят через YupResolver, так как resolver вызывается до отправки данных. Поэтому их обработка всегда выполняется отдельно.


Конфликт серверной и клиентской валидации

Ситуации, когда Yup и сервер возвращают разные ошибки для одного поля, требуют приоритизации:

  • клиентская ошибка → блокирует отправку
  • серверная ошибка → уточняет бизнес-логику

Пример конфликта:

  • Yup: email must be valid
  • Server: email domain is blocked

В таких случаях серверная ошибка должна перезаписывать или дополнять клиентскую.


Асинхронные проверки и YupResolver

Хотя YupResolver в основном синхронный, Yup поддерживает асинхронные проверки через test:

const schema = yup.object({
  email: yup
    .string()
    .email()
    .test("check-email", "Email already exists", async (value) => {
      const res = await api.checkEmail(value);
      return res.available;
    })
});

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


Контекстные ошибки и расширение YupResolver

YupResolver поддерживает передачу context, что позволяет учитывать внешние параметры:

const resolver = yupResolver(schema, {
  context: {
    mode: "create"
  }
});

Это используется для:

  • различий create/update
  • условных правил
  • динамических ограничений

Однако серверные ошибки всё равно остаются вне этой системы и должны обрабатываться отдельно.


Вложенные структуры и массивы ошибок

При работе с вложенными объектами важно сохранять путь поля:

{
  "errors": {
    "profile.email": "Invalid email",
    "addresses[0].city": "Required"
  }
}

Маппинг:

function mapNestedErrors(errors) {
  return Object.entries(errors).map(([path, message]) => ({
    field: path,
    message
  }));
}

Такая структура позволяет setError корректно привязать сообщение к глубоко вложенным полям.


Повторная отправка и очистка ошибок

После повторной отправки формы необходимо сбрасывать серверные ошибки, чтобы не оставались устаревшие сообщения:

function clearServerErrors(fields) {
  fields.forEach(field => {
    clearErrors(field);
  });
}

Или полностью:

clearErrors();

Консистентность контрактов API

Наиболее стабильная интеграция достигается при фиксированном формате ошибок со стороны сервера:

  • единый ключ errors
  • предсказуемая структура field → message
  • отсутствие вложенных вариаций форматов

Без этого слоя нормализации количество edge-case сценариев увеличивается экспоненциально.


Влияние серверных ошибок на UX формы

Серверные ошибки выполняют роль финального фильтра данных, но их отображение должно быть согласовано с состоянием формы:

  • ошибки не должны дублироваться на каждом submit
  • сообщения должны привязываться к конкретным полям
  • глобальные ошибки должны отображаться отдельно от полей
  • состояние загрузки должно блокировать повторную отправку до завершения запроса

Ошибки в сложных структурах данных

Для массивов и динамических форм сервер часто возвращает индексы:

{
  "errors": {
    "items[2].price": "Must be greater than 0"
  }
}

Такая структура требует точного соответствия путей, иначе React Hook Form не сможет корректно привязать ошибку.


Глобальные серверные ошибки и их обработка

Не все ошибки привязаны к полям:

{
  "message": "You do not have permission"
}

В таких случаях используется:

setError("root", {
  type: "server",
  message: "You do not have permission"
});

Или отдельное состояние:

setFormError(message);

Приоритеты обработки ошибок в форме

  1. Yup validation (до отправки)
  2. Resolver (структурная проверка)
  3. Server validation (бизнес-логика)
  4. UI mapping (setError)
  5. Rendering состояния ошибок

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