Последовательность операций с async и await
В прошлом уроке загрузка каталога проходила по цепочке: HTTP-ответ, проверка статуса, разбор JSON, проверка модели и создание карточек. Этот порядок сохраняется. Теперь запишем зависимые этапы как последовательность инструкций с async и await, чтобы условия и обработка ошибки находились рядом с операцией.
Работаем с тем же catalog-lab: четыре записи в data/courses.json, модули модели и представления, элементы #catalog-list и #catalog-status. Нового формата данных и новых зависимостей не появляется. Нужен современный браузер с поддержкой выбранных стандартных механизмов и просмотр проекта через HTTP. Учебный код при подготовке не выполнялся; ниже разбираются ожидаемые состояния и способы ручного наблюдения.
Асинхронная функция возвращает Promise
Ключевое слово async перед функцией меняет договор её вызова: результат представлен Promise. Даже если внутри возвращается обычное число, вызывающий код получает обещание этого значения. Поэтому async нельзя рассматривать только как разрешение поставить await в середину существующей функции без изменения её использования.
Для короткого самостоятельного наблюдения временно замените js/app.js следующим полным модулем. Импорты здесь не нужны:
async function courseCount() {
return 4;
}
const operation = courseCount();
console.log(operation instanceof Promise);
operation.then(value => console.log(`Курсов: ${value}`));
console.log('Вызов зарегистрирован');
Ожидаем сначала true, затем сообщение о регистрации вызова и после этого Курсов: 4. При печати самого operation консоль покажет представление Promise, внешний вид которого зависит от браузера. Не используйте его диагностическое оформление как часть контракта программы. Важен объект результата и способ получения числа.
Это не означает, что каждое выражение асинхронной функции переносится на потом. Её обычные инструкции начинают выполняться при вызове. await приостанавливает дальнейшую работу этой функции до результата ожидаемого значения, позволяя другой работе продолжаться. Семантика объявления изложена в справочнике async function.
Если функция выполняет долгий синхронный цикл до await, добавление async не делает цикл работой другого потока. Страница всё ещё может задерживать реакцию на действия пользователя. В нашем загрузчике основное ожидание связано с сетью и чтением тела; проверка четырёх записей остаётся маленькой синхронной операцией.
Тот же загрузчик в другой записи
Теперь замените весь js/api.js этим вариантом. Экспортированное имя loadCourses и адрес по умолчанию сохраняются, поэтому остальным модулям не требуется узнавать внутренний способ записи:
import { validateCourses } from './model.js';
export async function loadCourses(url = './data/courses.json') {
const response = await fetch(url, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const value = await response.json();
return validateCourses(value);
}
Первый await даёт функции дождаться объекта ответа. Только после этого имеет смысл читать response.ok и response.status. Второй нужен для результата чтения JSON. Если убрать его, в value будет Promise, а validateCourses ожидает массив и отвергнет другое значение. Запись выглядит последовательной, потому что это действительно зависимые этапы: проверять записи до разбора ответа нельзя.
return validateCourses(value) передаёт вызывающему коду проверенный массив как успешный результат обещания. При ошибке сети, чтения JSON или модели выполнение не достигает успешного возврата. Исключение внутри асинхронной функции отклоняет её Promise; выбор пользовательского сообщения остаётся за интерфейсом.
Сравните с предыдущей цепочкой then: оба варианта возвращают Promise проверенных записей. Программа не получает новый способ обработки 404 и не становится быстрее только из-за await. Мы изменили выражение зависимостей, а не правила HTTP. Поэтому проверка response.ok остаётся обязательным этапом именно нашего загрузчика.
Старайтесь держать одну ответственность на границе модуля. Здесь api.js получает и проверяет данные, но не ищет DOM-элементы и не пишет сообщения на странице. Благодаря этому интерфейс сам решит, сохранить прежние карточки или очистить их после сбоя. В итоговом учебном проекте выберем явную очистку, чтобы старые данные не выглядели свежим успешным ответом.
Ошибка там, где ожидают результат
Для интерфейса используем try, catch и finally. Следующий листинг целиком заменяет js/app.js; это ещё вариант с одной загрузкой при открытии. Форма остаётся отключённой до соединения всех действий в следующем уроке:
import { loadCourses } from './api.js';
import { countLessons } from './model.js';
import { renderCourses } from './view.js';
const list = document.querySelector('#catalog-list');
const status = document.querySelector('#catalog-status');
const fieldset = document.querySelector('#catalog-controls fieldset');
if (!list || !status) throw new Error('Не найден список или статус');
if (fieldset) fieldset.disabled = true;
async function refreshCatalog() {
list.setAttribute('aria-busy', 'true');
status.textContent = 'Загружаем каталог…';
try {
const courses = await loadCourses();
renderCourses(list, courses);
status.textContent = `Курсов: ${courses.length}. Уроков: ${countLessons(courses)}.`;
} catch (error) {
renderCourses(list, []);
status.textContent = 'Каталог не удалось загрузить.';
console.error('Ошибка загрузки:', error);
} finally {
list.removeAttribute('aria-busy');
}
}
refreshCatalog();
Оператор await loadCourses() внутри try связывает отклонение обещания с обработкой исключения. Поэтому catch получает не только явное throw при 404, но и ошибку разбора или проверки данных. Успешный путь создаёт карточки и сумму 60; путь сбоя очищает список и показывает сообщение. Мы сохраняем диагностическую причину в консоли, а пользователю даём понятное описание состояния.
finally снимает признак занятости в обоих случаях. Атрибут aria-busy обозначает временное изменение области для вспомогательных технологий; он сам не добавляет индикатор загрузки и не отключает взаимодействие. Не путайте семантическое описание состояния с видимым оформлением. В интерфейсе сообщение и реальное управление элементами остаются отдельными действиями.
В данном finally нет return и новой ошибки. Это намеренно: такой блок предназначен завершить сопутствующее состояние. Если вернуть из него новое значение или выбросить исключение, можно изменить исход предыдущей работы и скрыть первоначальную причину. В нашем маленьком примере очистка атрибута не требует такого вмешательства.
Почему внешний try недостаточен
Рассмотрим отдельный неправильный фрагмент; его не нужно добавлять к работающему приложению:
try {
loadCourses('./data/missing.json');
} catch (error) {
console.error('Сбой:', error);
}
Функция возвращает Promise, а try в этом фрагменте не ожидает его исход. Поэтому последующее отклонение не становится пойманным исключением этого синхронного блока. Нужно либо поставить await внутри асинхронной функции, либо прикрепить catch к возвращённому обещанию. Именно место ожидания связывает две модели обработки ошибок.
Есть и другая полезная граница: обработанный сбой не обязательно продолжает распространяться. Наш refreshCatalog ловит ошибку и завершает функцию нормально, потому что уже обновил интерфейс. Это выбранный договор функции представления. loadCourses, напротив, не превращает сбой в пустой массив: вызывающий код обязан различить ошибку и успешные данные. Такое разделение предотвращает сообщение «курсов нет», когда источник вообще недоступен.
Для ручного наблюдения восстановите правильный JSON, затем по очереди повторите условия предыдущего урока: отсутствующий путь, неверный синтаксис, неверное поле. Во всех случаях после завершения попытки атрибут занятости должен исчезнуть, а новое состояние соответствовать результату. Это ожидаемые условия проверки, не выполненный протокол. В следующем уроке добавим повторную загрузку и подключим отбор, сохранив один загрузочный запрос одновременно.