JavaScript / 38 минут

Промисы и async/await

Пишите асинхронные функции и запускайте независимые операции вместе.

В процессе Теория: впереди Практика: впереди Тест: впереди
15 / 22
После этой главы
  • Пишите асинхронные функции и запускайте независимые операции вместе.
  • Применить тему в задаче «Промисы и async/await» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Почему ожидание нужно отделять от вычисления

Чтение ответа сервера, таймер и некоторые операции среды завершаются не сразу. Если ждать их синхронным циклом, поток будет занят и не сможет обрабатывать другие события. Асинхронный API позволяет зарегистрировать продолжение и отдать управление среде. Это не означает, что любой код автоматически выполняется параллельно на другом процессоре: обычные JavaScript-инструкции всё ещё занимают выполняющий их поток.

Promise описывает будущий результат операции. Он может ожидать, завершиться успешно значением или завершиться отказом с причиной. После завершения состояние не меняется обратно. Promise не является самим значением, поэтому арифметика с ним вместо ожидания результата обычно ошибочна. Сначала нужно получить итог через then или await, а уже затем использовать полученные данные.

Создание и цепочка Promise

Конструктор Promise получает executor, который запускается синхронно. resolve завершает Promise успешно, reject отклоняет. Callbacks then и catch выполняются как асинхронные продолжения. Метод then возвращает новый Promise: возвращённое из callback значение становится его результатом, а исключение — причиной отказа. Это позволяет собирать последовательность зависимых шагов.

JavaScript
Promise.resolve(3)
  .then(value => value * 2)
  .then(value => console.log(value)); // 6

Если callback запускает асинхронную операцию, её Promise нужно вернуть, чтобы следующая часть цепочки дождалась завершения. Иначе получится оторванная работа, успех и ошибки которой не связаны с основной последовательностью. Особенно внимательно проверяйте фигурные скобки стрелки: внутри блока return не появляется автоматически.

async и await

async-функция всегда возвращает Promise. Обычное возвращённое значение становится успешным результатом Promise, а выброшенное исключение — отказом. await приостанавливает продолжение текущей async-функции до завершения ожидаемого значения, но не блокирует всю среду. После await код снова выполняется последовательно относительно этой функции.

JavaScript
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 не перехватит будущий асинхронный отказ этой работы. Связь между запуском и ожиданием нужна не только для порядка, но и для правильной обработки ошибок.

JavaScript
async function loadName() {
  try {
    const result = await Promise.reject(new Error('offline'));
    return result;
  } catch {
    return 'Недоступно';
  }
}

Не превращайте все ошибки в пустой успешный массив, если вызывающему коду важно различить отсутствие данных и сбой загрузки. Иногда лучший выбор — пробросить ошибку до границы интерфейса, где есть состояние error и действие «повторить». Контракт должен описывать ожидаемый отказ так же ясно, как успешный результат.

Несколько операций: последовательность и совместное ожидание

Если каждый шаг зависит от предыдущего, await внутри последовательного цикла оправдан. Если операции независимы, их можно запустить вместе и дождаться через Promise.all. Результаты Promise.all расположены в порядке исходного списка, а не в порядке фактического завершения. При отказе одного входного Promise общий Promise отклоняется, но уже запущенная работа остальных не отменяется автоматически.

JavaScript
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. Для самостоятельной тренировки добавьте разную задержку элементам и убедитесь, что порядок ответа сохраняется. Затем обсудите, как отличался бы контракт при необходимости вернуть результаты вместе с индивидуальными ошибками.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0