JavaScript / 38 минут
Промисы и async/await
Пишите асинхронные функции и запускайте независимые операции вместе.
- Пишите асинхронные функции и запускайте независимые операции вместе.
- Применить тему в задаче «Промисы и async/await» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Почему ожидание нужно отделять от вычисления
Чтение ответа сервера, таймер и некоторые операции среды завершаются не сразу. Если ждать их синхронным циклом, поток будет занят и не сможет обрабатывать другие события. Асинхронный API позволяет зарегистрировать продолжение и отдать управление среде. Это не означает, что любой код автоматически выполняется параллельно на другом процессоре: обычные JavaScript-инструкции всё ещё занимают выполняющий их поток.
Promise описывает будущий результат операции. Он может ожидать, завершиться успешно значением или завершиться отказом с причиной. После завершения состояние не меняется обратно. Promise не является самим значением, поэтому арифметика с ним вместо ожидания результата обычно ошибочна. Сначала нужно получить итог через then или await, а уже затем использовать полученные данные.
Создание и цепочка Promise
Конструктор Promise получает executor, который запускается синхронно. resolve завершает Promise успешно, reject отклоняет. Callbacks then и catch выполняются как асинхронные продолжения. Метод then возвращает новый Promise: возвращённое из callback значение становится его результатом, а исключение — причиной отказа. Это позволяет собирать последовательность зависимых шагов.
Promise.resolve(3)
.then(value => value * 2)
.then(value => console.log(value)); // 6Если callback запускает асинхронную операцию, её Promise нужно вернуть, чтобы следующая часть цепочки дождалась завершения. Иначе получится оторванная работа, успех и ошибки которой не связаны с основной последовательностью. Особенно внимательно проверяйте фигурные скобки стрелки: внутри блока return не появляется автоматически.
async и await
async-функция всегда возвращает Promise. Обычное возвращённое значение становится успешным результатом Promise, а выброшенное исключение — отказом. await приостанавливает продолжение текущей async-функции до завершения ожидаемого значения, но не блокирует всю среду. После await код снова выполняется последовательно относительно этой функции.
async function doubled(value) {
const received = await Promise.resolve(value);
return received * 2;
}
console.log(await doubled(4)); // 8 в среде с top-level awaitВ обычном немодульном фрагменте top-level await может быть недоступен. Тогда вызов помещают в async-функцию или используют then. Среда и формат файла снова имеют значение. Не добавляйте async только ради внешнего вида: сначала определите, какой шаг действительно асинхронен и кто должен дождаться результата.
Ошибки асинхронного вычисления
Отказ ожидаемого Promise в точке await ведёт себя как исключение внутри async-функции. Его можно перехватить обычным try/catch. Но catch вокруг запуска Promise без await не перехватит будущий асинхронный отказ этой работы. Связь между запуском и ожиданием нужна не только для порядка, но и для правильной обработки ошибок.
async function loadName() {
try {
const result = await Promise.reject(new Error('offline'));
return result;
} catch {
return 'Недоступно';
}
}Не превращайте все ошибки в пустой успешный массив, если вызывающему коду важно различить отсутствие данных и сбой загрузки. Иногда лучший выбор — пробросить ошибку до границы интерфейса, где есть состояние error и действие «повторить». Контракт должен описывать ожидаемый отказ так же ясно, как успешный результат.
Несколько операций: последовательность и совместное ожидание
Если каждый шаг зависит от предыдущего, await внутри последовательного цикла оправдан. Если операции независимы, их можно запустить вместе и дождаться через Promise.all. Результаты Promise.all расположены в порядке исходного списка, а не в порядке фактического завершения. При отказе одного входного Promise общий Promise отклоняется, но уже запущенная работа остальных не отменяется автоматически.
const inputs = [3, 1, 2];
const results = await Promise.all(inputs.map(async value => value * 10));
console.log(results); // [30, 10, 20]Promise.allSettled полезен, когда нужно получить отдельный итог каждой операции независимо от отказов соседей. Promise.race выбирает первое завершение, а Promise.any — первое успешное завершение с соответствующим поведением общего отказа. Эти инструменты не заменяют ограничение конкурентности: запуск тысячи сетевых операций одновременно может перегрузить ресурсы.
Практика и типичная ловушка map
В задании требуется асинхронно удвоить элементы массива и вернуть готовые числа в исходном порядке. map с async-callback сам по себе возвращает массив Promise, а не массив чисел. Нужно отдельно дождаться всего набора. На пустом массиве Promise.all успешно даст пустой массив; это естественное поведение, не требующее случайного исключения.
Проверьте ноль, отрицательные числа и несколько элементов. После решения объясните, что возвращает callback, что возвращает map и что возвращает await Promise.all. Для самостоятельной тренировки добавьте разную задержку элементам и убедитесь, что порядок ответа сохраняется. Затем обсудите, как отличался бы контракт при необходимости вернуть результаты вместе с индивидуальными ошибками.