Обработка ошибок

Система валидации в библиотеке Vest строится вокруг декларативных тестов, где ошибки являются результатом выполнения сценария (suite). В отличие от императивных валидаторов, Vest хранит состояние проверок, отслеживает проваленные тесты и предоставляет централизованный API для получения информации об ошибках.

Ошибки в Vest делятся на несколько категорий:

  • ошибки отдельных тестов;
  • ошибки групп;
  • асинхронные ошибки;
  • предупреждения (warn);
  • системные ошибки исполнения;
  • ошибки при динамической валидации форм.

Библиотека не выбрасывает исключения для обычных ошибок валидации. Вместо этого создаётся объект состояния (suite result), содержащий сведения о проваленных проверках.


Базовая обработка ошибок

Получение ошибок через getErrors

import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/^\S+@\S+\.\S+$/);
  });

  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThanOrEquals(8);
  });
});

const result = suite({
  email: 'wrong',
  password: '123'
});

console.log(result.getErrors());

Результат:

{
  email: ['Некорректный email'],
  password: ['Пароль слишком короткий']
}

Метод getErrors() возвращает объект, где:

  • ключ — имя поля;
  • значение — массив сообщений об ошибках.

Даже если ошибка одна, Vest всегда использует массив.


Получение ошибок конкретного поля

Метод getErrors(fieldName)

const emailErrors = result.getErrors('email');

console.log(emailErrors);

Результат:

['Некорректный email']

Это особенно полезно при интеграции с UI-компонентами.


Проверка наличия ошибок

Метод hasErrors

if (result.hasErrors()) {
  console.log('Форма содержит ошибки');
}

Проверка конкретного поля:

if (result.hasErrors('email')) {
  console.log('Ошибка email');
}

Получение первого сообщения ошибки

Метод getError

const error = result.getError('email');

console.log(error);

Результат:

'Некорректный email'

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


Проверка валидности

Метод isValid

if (result.isValid()) {
  console.log('Форма валидна');
}

Проверка конкретного поля:

result.isValid('password');

Метод является инверсией hasErrors.


Формирование сообщений об ошибках

Статические сообщения

test('username', 'Имя пользователя обязательно', () => {
  enforce(data.username).isNotBlank();
});

Динамические сообщения

Сообщение может зависеть от данных.

test('age', () => {
  const age = data.age;

  enforce(age).greaterThan(17);
}, `Возраст ${data.age} недопустим`);

Генерация сообщений внутри теста

test('password', () => {
  if (data.password.length < 8) {
    throw new Error('Минимум 8 символов');
  }
});

Vest автоматически перехватывает Error.


Работа с исключениями

Исключения внутри тестов

Если внутри test возникает ошибка:

test('profile', () => {
  JSON.parse('{ invalid json }');
});

Vest помечает тест как проваленный.


Отличие ошибок валидации от системных ошибок

Ошибки валидации:

enforce(data.age).greaterThan(18);

Системные ошибки:

const x = undefined.name;

Vest умеет обрабатывать оба типа, но системные ошибки желательно логировать отдельно.


Перехват ошибок вручную

test('config', () => {
  try {
    riskyOperation();
  } catch (e) {
    throw new Error('Ошибка обработки конфигурации');
  }
});

Асинхронные ошибки

Асинхронные тесты

test('email', 'Email уже используется', async () => {
  const exists = await api.checkEmail(data.email);

  enforce(exists).isFalsy();
});

Vest отслеживает состояние Promise и обновляет результат после завершения.


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

Метод isPending

if (result.isPending()) {
  console.log('Проверка выполняется');
}

Для поля:

result.isPending('email');

Ошибки отклонённых Promise

test('server', async () => {
  await fetchData();
});

Если fetchData() выбросит исключение или вернёт rejected Promise, тест завершится ошибкой.


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

test('email', async () => {
  try {
    await api.validateEmail(data.email);
  } catch (e) {
    throw new Error('Сервер недоступен');
  }
});

Предупреждения вместо ошибок

Использование warn

Vest поддерживает предупреждения, не блокирующие валидность формы.

import { create, test, warn } from 'vest';

const suite = create(data => {
  test('password', 'Слабый пароль', () => {
    warn();

    enforce(data.password).longerThanOrEquals(12);
  });
});

Проверка предупреждений

Метод hasWarnings

result.hasWarnings();

Для поля:

result.hasWarnings('password');

Получение предупреждений

result.getWarnings();

Или:

result.getWarnings('password');

Группировка ошибок

Использование group

import { group } from 'vest';

group('auth', () => {
  test('email', 'Неверный email', () => {
    enforce(data.email).matches(/@/);
  });

  test('password', 'Слишком короткий пароль', () => {
    enforce(data.password).longerThan(7);
  });
});

Проверка ошибок группы

result.hasErrorsByGroup('auth');

Получение ошибок:

result.getErrorsByGroup('auth');

Управление отображением ошибок

Отложенное отображение через only

suite(data, 'email');

Выполняется только проверка поля email.


Частичная валидация

suite(data, ['email', 'password']);

Ошибки остальных полей игнорируются.


Исключение проверок через skip

import { skipWhen } from 'vest';

skipWhen(result.hasErrors('email'), () => {
  test('password', 'Пароль обязателен', () => {
    enforce(data.password).isNotBlank();
  });
});

Кастомизация ошибок

Собственные валидаторы

enforce.extend({
  isPhone(value) {
    return /^\+\d+$/.test(value);
  }
});

Использование:

test('phone', 'Телефон некорректен', () => {
  enforce(data.phone).isPhone();
});

Унификация сообщений

const messages = {
  required: 'Поле обязательно',
  invalidEmail: 'Некорректный email'
};
test('email', messages.invalidEmail, () => {
  enforce(data.email).matches(/@/);
});

Локализация ошибок

Использование словаря переводов

const i18n = {
  ru: {
    required: 'Поле обязательно'
  },
  en: {
    required: 'Field is required'
  }
};

Динамическая локализация

test('name', i18n[locale].required, () => {
  enforce(data.name).isNotBlank();
});

Агрегация ошибок

Сбор всех ошибок формы

const allErrors = result.getErrors();

Преобразование структуры

const formatted = Object.entries(result.getErrors())
  .map(([field, errors]) => ({
    field,
    errors
  }));

Работа с UI

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

{result.hasErrors('email') && (
  <span>{result.getError('email')}</span>
)}

Отображение нескольких ошибок

<ul>
  {result.getErrors('password').map(error => (
    <li key={error}>{error}</li>
  ))}
</ul>

Обработка ошибок при динамических формах

Проверка массивов

data.users.forEach((user, index) => {
  test(`users.${index}.email`, 'Некорректный email', () => {
    enforce(user.email).matches(/@/);
  });
});

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

result.getErrors('users.0.email');

Состояние выполнения тестов

Проверка завершённости

result.isPending();

Проверка запуска теста

result.tested('email');

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


Условная обработка ошибок

Валидация зависимых полей

test('password', 'Пароль обязателен', () => {
  enforce(data.password).isNotBlank();
});

skipWhen(result.hasErrors('password'), () => {
  test('confirmPassword', 'Пароли не совпадают', () => {
    enforce(data.confirmPassword).equals(data.password);
  });
});

Раннее завершение проверок

Использование omitWhen

import { omitWhen } from 'vest';

omitWhen(data.isGuest, () => {
  test('address', 'Адрес обязателен', () => {
    enforce(data.address).isNotBlank();
  });
});

Если условие истинно, тесты не попадут в итоговый результат вообще.


Управление критическими ошибками

Критические проверки

test('token', 'Токен недействителен', () => {
  enforce(data.token).isNotBlank();
});

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


Отладка ошибок

Логирование результата

console.log(result);

Объект содержит:

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

Проверка конкретного теста

result.tested('email');

Архитектура обработки ошибок

Централизованная схема

Распространённый подход:

const validationResult = suite(data);

const errors = validationResult.getErrors();

return {
  valid: validationResult.isValid(),
  errors
};

Разделение ошибок

Обычно ошибки делят на:

  • ошибки ввода;
  • серверные ошибки;
  • предупреждения;
  • системные исключения;
  • ошибки бизнес-логики.

Vest позволяет хранить их в едином пайплайне валидации.


Типичные ошибки при работе с Vest

Отсутствие уникальных имён тестов

Плохо:

test('field', 'Ошибка 1', () => {});
test('field', 'Ошибка 2', () => {});

Лучше:

test('emailFormat', 'Неверный email', () => {});
test('emailRequired', 'Email обязателен', () => {});

Смешивание системных ошибок и ошибок валидации

Плохо:

test('data', () => {
  riskyFunction();
});

Лучше:

test('data', () => {
  try {
    riskyFunction();
  } catch {
    throw new Error('Ошибка обработки данных');
  }
});

Игнорирование асинхронного состояния

Плохо:

const result = suite(data);

console.log(result.isValid());

При наличии async-тестов результат может быть ещё не готов.

Правильно:

if (!result.isPending()) {
  console.log(result.isValid());
}

Практический пример комплексной обработки ошибок

import {
  create,
  test,
  enforce,
  warn,
  group
} from 'vest';

const suite = create(async data => {
  group('auth', () => {
    test('email', 'Email обязателен', () => {
      enforce(data.email).isNotBlank();
    });

    test('email_format', 'Неверный email', () => {
      enforce(data.email).matches(/^\S+@\S+\.\S+$/);
    });

    test('password', 'Минимум 8 символов', () => {
      enforce(data.password).longerThanOrEquals(8);
    });

    test('password_strength', 'Слабый пароль', () => {
      warn();

      enforce(data.password).longerThanOrEquals(12);
    });

    test('email_unique', async () => {
      const exists = await api.emailExists(data.email);

      enforce(exists).isFalsy();
    }, 'Email уже используется');
  });
});

const result = suite({
  email: 'wrong',
  password: '123'
});

console.log(result.getErrors());
console.log(result.getWarnings());
console.log(result.hasErrorsByGroup('auth'));