Промисы и async/await: разбор на живом примере с гонками
Три fetch подряд ждут друг друга и страница грузится секунды вместо миллисекунд. Разбираем, где async/await прячет параллелизм и как промисы ведут себя под капотом.

Задача простая: страница профиля тянет три эндпоинта — данные пользователя, его заказы и настройки. Три await fetch() подряд, всё работает. Но профиль грузится 900 мс, хотя каждый запрос отвечает за 300 мс. Проблема не в сети и не в бэкенде — в том, что await заставил три независимых запроса выстроиться в очередь. Разберём, что происходит на уровне промисов, где async/await скрывает от вас лишнее ожидание и какие ловушки вылезают в проде. Примеры на Node.js 20+ и в современных браузерах (ES2022), где Promise и async/await ведут себя одинаково.
Что такое промис на самом деле
Промис — это объект-обёртка над значением, которого пока нет. У него три состояния: pending (в процессе), fulfilled (успешно завершён) и rejected (завершён с ошибкой). Из pending он переходит в одно из двух финальных состояний ровно один раз и больше не меняется. Это ключевое свойство: промис нельзя «перезапустить» или отменить сменой состояния.
Важная деталь, которую пропускают: промис начинает выполнять свою работу в момент создания, а не в момент, когда вы вешаете на него .then() или пишете await. Вызвали fetch() — запрос уже полетел. await лишь говорит «подожди здесь результата», но не запускает работу.
Промис — это не «ленивое» вычисление. Он стартует сразу при создании. await управляет только тем, где вы дождётесь результата, а не тем, когда работа началась.
Как async/await соотносится с промисами
Функция, помеченная async, всегда возвращает промис — даже если внутри вы вернули обычное число. await — это синтаксис поверх .then(): он приостанавливает выполнение async-функции до разрешения промиса и возвращает его значение. Никакой отдельной магии здесь нет, это тот же промис, просто записанный без цепочек .then().
Живой пример: три запроса, которые ждут зря
Вот тот самый медленный код. Он корректен, но каждый await блокирует запуск следующего запроса:
// Node.js 20+, глобальный fetch доступен без импорта
async function loadProfileSlow(userId) {
const userRes = await fetch(`/api/users/${userId}`);
const user = await userRes.json();
// этот fetch стартует только после того,
// как полностью завершился запрос выше
const ordersRes = await fetch(`/api/users/${userId}/orders`);
const orders = await ordersRes.json();
const settingsRes = await fetch(`/api/users/${userId}/settings`);
const settings = await settingsRes.json();
return { user, orders, settings };
}
Запросы независимы: заказам не нужны настройки, настройкам не нужен ответ по пользователю. Но await на первой строке останавливает функцию, пока запрос не вернётся. Только потом стартует второй. Итог — 300 + 300 + 300 = 900 мс последовательного ожидания.
Как исправить: запускаем всё сразу
Промис стартует при создании. Значит, создадим все три промиса до первого await, а дождёмся их вместе через Promise.all:
async function loadProfileFast(userId) {
// все три запроса стартуют почти одновременно
const [userRes, ordersRes, settingsRes] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/orders`),
fetch(`/api/users/${userId}/settings`),
]);
const [user, orders, settings] = await Promise.all([
userRes.json(),
ordersRes.json(),
settingsRes.json(),
]);
return { user, orders, settings };
}
Теперь общее время — примерно 300 мс плюс накладные расходы, а не 900. Запросы идут параллельно, ограничены только тем, сколько одновременных соединений допускает браузер или сервер.
Где какой инструмент
У промисов несколько комбинаторов, и путаница между ними — источник тонких багов. Разница в том, как они реагируют на ошибку одного из промисов.
| Метод | Когда разрешается | Что при ошибке одного |
|---|---|---|
Promise.all | Когда завершились все | Сразу reject первой ошибкой, остальные результаты теряются |
Promise.allSettled | Когда завершились все | Не отклоняется, возвращает статус каждого |
Promise.race | Когда завершился первый (любой) | Reject, если первым завершился отклонённый |
Promise.any | Когда завершился первый успешный | Reject только если провалились все |
Практический вывод: если один упавший запрос не должен ронять всю загрузку — берите allSettled и разбирайте статусы вручную. Если нужен любой из нескольких зеркал — any. race удобен для таймаутов.
Таймаут через Promise.race
Классический приём: гонка полезного промиса с таймером. Кто первый — тот и результат.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Таймаут ${ms} мс`)), ms)
);
return Promise.race([promise, timeout]);
}
// использование
const data = await withTimeout(fetch('/api/slow'), 2000);
Учтите: race не отменяет проигравший промис. Запрос продолжит выполняться в фоне, даже если вы уже отреагировали на таймаут. Для реальной отмены нужен AbortController — это отдельная тема, здесь её не разворачиваем.
Ошибки, на которых спотыкаются в проде
Синтаксис async/await выглядит как обычный последовательный код, и именно это усыпляет бдительность. Вот что ломается чаще всего.
- Забытый
await. Написалиconst orders = fetchOrders()безawait— вordersлежит промис, а не данные. Дальшеorders.lengthвернётundefined, а не упадёт с ошибкой. Баг тихий. forEachс async-колбэком.arr.forEach(async item => await save(item))не ждёт ничего:forEachигнорирует возвращаемые промисы. Код едет дальше, пока сохранения ещё идут. Используйтеfor...ofсawaitвнутри илиPromise.all(arr.map(...)).- Непойманный reject. Промис без обработки ошибки даёт
UnhandledPromiseRejection. В Node.js 20+ по умолчанию это завершает процесс. Оборачивайтеawaitвtry/catchили вешайте.catch(). - Потеря контекста в
Promise.all. Если один из промисов упадёт, вы получите ошибку, но не узнаете, какой именно из них —allне сообщает индекс. Для диагностики оборачивайте каждый в свойcatchс меткой или беритеallSettled.
Последовательность там, где она реально нужна
Параллельность — не всегда добро. Если второй запрос зависит от результата первого (например, сначала получить ID, потом по нему тянуть детали), последовательный await — правильный выбор, а не баг. Ошибка — параллелить зависимые операции, ошибка же — сериализовать независимые. Смотрите на граф зависимостей данных, а не на то, «красивее» ли выглядит код.
Микрозадачи: почему порядок выполнения не всегда очевиден
Разрешённый промис не выполняет .then() немедленно. Колбэк ставится в очередь микрозадач и выполняется после того, как отработает текущий синхронный код. Это объясняет неожиданный порядок вывода:
console.log('1 синхронно');
Promise.resolve().then(() => console.log('3 микрозадача'));
console.log('2 синхронно');
// вывод: 1 синхронно, 2 синхронно, 3 микрозадача
Практический смысл: не рассчитывайте, что значение из .then() будет доступно сразу на следующей строке без await. И помните, что setTimeout (макрозадача) всегда отработает позже микрозадач, даже с нулевой задержкой.
Что делать дальше
Пройдите по своему коду и найдите цепочки await, где запросы независимы, — это первый кандидат на ускорение через Promise.all. Замерьте до и после через performance.now() или вкладку Network в DevTools: параллельные запросы видно по одновременно стартующим полоскам. Следующий шаг после этого — освоить AbortController для реальной отмены запросов по таймауту и посмотреть в сторону структурной обработки ошибок через allSettled там, где падение одного вызова не должно ронять всю страницу.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Using Promises — MDN Web Docs, Promise — MDN Web Docs
Частые вопросы
А если у меня старая версия Node без глобального fetch?
Promise.all упал с ошибкой, но я не понимаю, какой запрос виноват. Как отладить?
Чем await отличается от .then() — это просто разный синтаксис?
Почему код после забытого await не падает, а просто работает неправильно?
Параллельный запуск через Promise.all не перегрузит сервер или браузер?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.