Перейти к содержанию

Ошибки и проверка данных каталога

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

В этом уроке введём функцию validateCourses. Она проверяет неизвестное значение и возвращает массив проверенных плоских записей. Вариант рассчитан на плотные массивы нашего каталога и JSON: разреженные массивы с пустыми позициями требуют дополнительного правила обхода. При обнаруженном нарушении функция выбрасывает исключение. Код, управляющий интерфейсом, позднее перехватит отказ и покажет понятное сообщение читателю.

Исключение как нарушение операции

Начнём с маленького полного примера вместо прежнего app.js:

function validateLessons(value) {
  if (!Number.isInteger(value) || value <= 0) {
    throw new TypeError('Число уроков должно быть положительным целым');
  }
  return value;
}
try {
  console.log(validateLessons('20'));
} catch (error) {
  console.error('Данные курса неверны:', error.message);
}

Ожидается сообщение о неверных данных. Строка '20' внешне похожа на количество, но контракт ожидает число. throw прекращает нормальное выполнение функции; вызывающее выражение не получает обычный результат. Управление переходит к ближайшему подходящему catch на пути выполнения.

Здесь TypeError содержит диагностическое сообщение для разработчика. Важнее причина ошибки, чем точное оформление консоли: браузеры выводят объекты исключений по-разному. Не выдаём ожидаемую фразу за результат уже выполненного запуска. При подготовке серии примеры ещё не исполнялись.

Если передать число 20, функция вернёт его и catch не выполнится. При числе 0 или 12.5 произойдёт тот же отказ, но уже из-за предметного правила количества. Проверка типа и проверка диапазона дополняют друг друга. Не преобразуем данные автоматически, потому что ошибка формата должна быть обнаружена в месте их получения.

Порядок проверки структуры

Полная функция проверки каталога выглядит так. Добавьте её перед обработкой массива courses из урока об объектах; перед этим восстановите само объявление четырёх курсов.

function validateCourses(value) {
  if (!Array.isArray(value)) throw new TypeError('Ожидался массив курсов');
  const ids = new Set();
  return value.map((course, index) => {
    if (course === null || typeof course !== 'object' || Array.isArray(course)) {
      throw new TypeError(`Запись ${index + 1}: ожидался объект`);
    }
    const { id, title, topic, lessons, url } = course;
    if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id) || ids.has(id)) {
      throw new TypeError(`Запись ${index + 1}: неверный или повторный id`);
    }
    if (typeof title !== 'string' || title.trim() === '') {
      throw new TypeError(`Запись ${index + 1}: пустое название`);
    }
    if (topic !== 'frontend' && topic !== 'publishing') {
      throw new TypeError(`Запись ${index + 1}: неизвестная тема`);
    }
    if (!Number.isInteger(lessons) || lessons <= 0) {
      throw new TypeError(`Запись ${index + 1}: неверное число уроков`);
    }
    if (typeof url !== 'string' || !/^\.\/courses\/[a-z0-9-]+\.html$/.test(url)) {
      throw new TypeError(`Запись ${index + 1}: недопустимый URL`);
    }
    ids.add(id);
    return { id, title: title.trim(), topic, lessons, url };
  });
}

Сначала проверяется массив, затем отдельная запись. Только после этого выполняется деструктуризация. Проверка на null нужна отдельно из-за особенностей typeof; массив тоже является объектом языка, но не соответствует принятой записи курса. Порядок не позволяет обратиться к полям значения, которое вообще не является карточкой.

Set хранит уже встреченные идентификаторы. Его метод has проверяет наличие, а add добавляет идентификатор после успешной проверки записи. Это новая небольшая коллекция для уникальности; она не заменяет упорядоченный массив карточек. Повторный id считается ошибкой независимо от того, различаются ли названия.

Два выражения между косыми чертами являются регулярными выражениями. Первое разрешает непустой идентификатор из строчных латинских букв, цифр и дефисов. Второе ограничивает ссылку конкретной учебной папкой и расширением HTML. Это узкие правила нашего fixture, а не универсальный валидатор любых URL.

Проверенные записи и граница catch

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

Для ручного наблюдения вызовите функцию внутри try и выведите длину результата. Ожидается четыре. Затем временно замените количество первой записи строкой, а после восстановления повторите её идентификатор в другой записи. Ожидаются разные сообщения с номером повреждённой записи. Сам номер здесь нужен для диагностики файла, а постоянная идентичность всё равно задаётся id.

Не оборачивайте каждое обращение к свойству отдельным пустым catch. Тогда программа может продолжить работу с частично обработанными данными, а истинная причина потеряется. В нашем каталоге проверка работает целиком: либо получен согласованный массив, либо загрузка объявлена неуспешной. Это упрощает объяснение состояния интерфейса.

Механизм исключений рассматривается в MDN о throw и MDN о try…catch. Наши правила полей описывают собственную модель каталога, а не требования этих конструкций языка.

Что считать неуспешным каталогом

Пустой массив и неверный массив — разные результаты. [] соответствует контракту нашей функции и означает отсутствие записей. Строка вместо массива нарушает контракт и вызывает исключение. В интерфейсе первое состояние может получить спокойную подпись об отсутствии курсов, второе — предложение повторить загрузку. Если подменить все исключения пустым массивом, эти причины станут неразличимыми.

Решение отклонять весь набор тоже является явной политикой. Можно было бы пропускать повреждённые записи и показывать остальные, однако тогда понадобится сообщать о частичном результате и учитывать неполный подсчёт. Учебный каталог выбирает более простой контракт: либо все записи проверены, либо результат не принят. Так число карточек не выглядит доказательством полноты ошибочного источника.

Исключение внутри map прекращает операцию до возвращения окончательного массива. Ранее созданные временные записи не передаются вызывающему коду как успешный результат. Поэтому исходное значение не нужно очищать вручную после каждой проверки. Мы не меняем его, а построение нового результата завершится только после обработки всех записей.

Ошибки в программе не следует путать с ожидаемой ошибкой данных. Например, опечатка в имени Number является ошибкой реализации, а строковое количество — отказом контракта. Внешний catch может технически поймать оба случая, но сохранённый объект ошибки позволит разработчику различить причины. Не заменяйте подробный журнал одной универсальной фразой без контекста.

Наконец, проверка URL здесь ограничивает допустимую форму ссылки, но не подтверждает наличие файла назначения. Чтобы подтвердить ресурс, потребуется отдельная ручная проверка страницы или проверка ссылок. Так же число двадцать подтверждает согласованный формат, но не доказывает, что все двадцать уроков реально написаны. Валидатор отвечает только за заявленную схему данных; редакционная и ресурсная полнота являются другими проверками.

Чтобы увидеть, где расположен отказ, используйте разные испорченные значения по одному. Сначала передайте строку вместо массива, затем массив с null, затем настоящую запись с пустым названием. Если изменить сразу три поля, первый обнаруженный отказ не покажет работу следующих проверок. Последовательный опыт даёт понятную связь причины и ожидаемого сообщения.

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

Узкое регулярное выражение ссылки специально исключает другие пути. Если новый курс действительно понадобится вне ./courses, придётся изменить договорённость и валидатор вместе. Нельзя сначала сделать функцию «принимающей всё», а затем считать прежнее ограничение выполненным. Контракт живёт в коде проверки и в объяснении модели одновременно; любое расширение требует согласованного обновления обоих мест.

Сообщение читателю позднее будет коротким: каталог не удалось загрузить, можно повторить. Детальный объект исключения останется в консоли разработчика. Человек не должен видеть вместо карточек техническую трассировку, но разработчик должен иметь возможность отличить неверный JSON от неизвестной темы. Теперь можем переходить к DOM, сохраняя эту границу между данными и отображением.

Оглавление курса