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

В связке с React Hook Form и Yup схема валидации выполняется на уровне resolver-функции, которая преобразует результат Yup.validate() в формат, понятный форме. Именно на этом этапе чаще всего теряется прозрачность ошибок, если не настроено корректное логирование.

Ключевой момент: YupResolver не выбрасывает ошибки наружу, а возвращает структурированный объект:

{
  values: T,
  errors: FieldErrors<T>
}

Отладка сводится к пониманию того, как именно Yup формирует ValidationError, и как resolver трансформирует его в FieldErrors.


Структура ошибок Yup и её влияние на resolver

Yup генерирует объект ValidationError, который содержит несколько важных полей:

  • message — текст ошибки
  • path — путь к полю формы
  • inner — массив вложенных ошибок (при abortEarly: false)
  • errors — список сообщений
  • value — исходное значение

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

Yup.object({
  email: Yup.string().email().required(),
  password: Yup.string().min(8).required(),
});

При ошибке password без abortEarly: false отладка может скрывать остальные проблемы формы.


Включение полного режима ошибок для отладки

Для расширенного логирования критично отключать ранний выход:

const schema = Yup.object({
  email: Yup.string().email().required(),
  password: Yup.string().min(8).required(),
}).validate(data, { abortEarly: false });

В контексте YupResolver это прокидывается через конфигурацию:

yupResolver(schema, {
  abortEarly: false,
});

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


Логирование внутри resolver через обёртку

Стандартный yupResolver скрывает внутреннюю обработку ошибок. Для отладки часто используется обёртка:

import { yupResolver } from '@hookform/resolvers/yup';

const debugResolver = (schema) => {
  const resolver = yupResolver(schema);

  return async (values, context, options) => {
    const result = await resolver(values, context, options);

    console.group('YupResolver Debug');
    console.log('Values:', values);
    console.log('Errors:', result.errors);
    console.groupEnd();

    return result;
  };
};

Такой подход позволяет отследить момент трансформации данных до попадания в React Hook Form.


Анализ ValidationError.inner

Наиболее информативная часть при сложных схемах:

catch (err) {
  if (err.name === 'ValidationError') {
    console.log(err.inner);
  }
}

Каждый элемент inner имеет:

  • path — ключ поля (user.email)
  • message — текст
  • type — тип правила (required, min, email)

Типичная проблема: несовпадение path с названием поля в форме. Это приводит к тому, что ошибка существует, но не отображается.


Несоответствие путей и вложенные объекты

При работе с вложенными схемами:

const schema = Yup.object({
  user: Yup.object({
    email: Yup.string().required(),
  }),
});

React Hook Form ожидает ошибки вида:

errors.user.email

Но Yup может вернуть path: "user.email", что требует корректного маппинга внутри resolver.

При кастомизации важно проверять:

  • формат dot notation
  • соответствие структуры defaultValues
  • наличие nullable() и defined()

Проблемы сериализации ошибок

При логировании часто теряется часть информации из-за:

  • JSON.stringify(err) (теряет методы и внутренние поля)
  • частичного копирования объекта
  • прокси-объектов React Hook Form

Корректный подход:

console.dir(err, { depth: null });

или выборочное логирование:

console.log({
  path: err.path,
  message: err.message,
  inner: err.inner,
});

Отладка через formState в React Hook Form

После выполнения resolver результат попадает в formState.errors:

const {
  formState: { errors },
} = useForm({
  resolver: yupResolver(schema),
});

Логирование на уровне компонента:

useEffect(() => {
  console.log('Form errors updated:', errors);
}, [errors]);

Это позволяет отследить, как именно resolver преобразовал результат Yup.


Перехват ошибок через кастомный resolver для диагностики типов

TypeScript иногда скрывает реальные структуры ошибок. Для диагностики используется явное расширение:

const safeResolver = async (values, context, options) => {
  try {
    const result = await yupResolver(schema)(values, context, options);

    if (Object.keys(result.errors).length) {
      console.warn('Validation errors:', result.errors);
    }

    return result;
  } catch (e) {
    console.error('Resolver crash:', e);
    throw e;
  }
};

Частые причины «пустых» ошибок

Несмотря на наличие ошибок в Yup, errors может быть пустым:

  • несовпадение структуры defaultValues и schema
  • отсутствие регистрации поля (register)
  • использование shouldUnregister: true
  • неправильный name в input
  • потеря ref в кастомных компонентах

Логирование асинхронных схем

При использовании Yup.lazy или асинхронных проверок:

Yup.string().test('async-check', async (value) => {
  const res = await apiCheck(value);
  return res.valid;
});

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

console.log('Validating value:', value);

и фиксации результата каждого test.


Инструментальная отладка через перехват validate

Иногда требуется исключить resolver и проверить Yup напрямую:

try {
  await schema.validate(data, { abortEarly: false });
} catch (e) {
  console.log(e.inner);
}

Это позволяет определить, где именно возникает расхождение: в Yup или в yupResolver.


Логирование производственных ошибок

В production-среде прямой console.log недостаточен. Используются внешние системы:

  • Sentry
  • LogRocket
  • Datadog

Ключевой подход — логировать только нормализованные данные:

{
  schema: 'userForm',
  errors: normalizeYupError(err),
  values: sanitize(values),
}

Функция нормализации обычно извлекает только path, message, type, исключая чувствительные данные.


Проблемы типизации ошибок в TypeScript

При использовании yupResolver типизация ошибок может расходиться с фактической структурой:

FieldErrors<T>

но Yup возвращает динамическую структуру. Для диагностики используется:

type DebugErrors = Record<string, any>;

или временное отключение строгой типизации при отладке.


Поведение resolver при нескольких ошибках на одном поле

Yup может вернуть несколько ошибок для одного path, но React Hook Form отображает только одну. Это приводит к потере информации.

Логирование inner позволяет увидеть полную картину:

err.inner.filter(e => e.path === 'email');

Диагностика производительности валидации

При сложных схемах логирование включает измерение времени:

const start = performance.now();
await resolver(values);
console.log('Validation time:', performance.now() - start);

Это помогает выявить тяжёлые участки схемы (matches, test, вложенные object).


Различие поведения development и production

В development режиме:

  • ошибки более подробные
  • сохраняется стек
  • inner массив полон данных

В production:

  • сообщения могут быть сокращены
  • стек отсутствует
  • оптимизации Yup упрощают объект ошибки

Логирование должно учитывать это различие, иначе диагностика становится неполной.