async/await — это просто промисы в красивой обёртке
async/await — это не новая модель асинхронности в JavaScript. Это синтаксический сахар поверх промисов: код выглядит так, будто выполняется последовательно, хотя на самом деле он асинхронный. Механика та же, просто форма гораздо приятнее.
Вот одна и та же задача, написанная двумя способами:
Обе функции возвращают промис. Обе делают ровно одно и то же. Но async-версия читается сверху вниз, без цепочки .then — собственно, в этом и весь смысл.
async помечает функцию как возвращающую промис
Добавьте async перед function, стрелочной функцией или методом — и происходят две вещи:
- Функция всегда возвращает промис. То, что вы
return, становится значением, с которым промис зарезолвится. - Внутри такой функции можно использовать
await.
Обратите внимание: result — это не строка, а промис, который разрешится строкой. Несмотря на то что внутри greet нет ни await, ни какой-либо асинхронной работы, ключевое слово async всё равно оборачивает возвращаемое значение в промис. Если функция бросит исключение — промис отклонится.
await ждёт, пока промис завершится
Внутри async-функции выражение await somePromise приостанавливает её выполнение до тех пор, пока промис не разрешится, а затем возвращает полученное значение. Если промис отклонён, await бросит исключение.
Обратите внимание на порядок вывода. "отсчёт запущен" печатается раньше, чем "2" — потому что await ставит на паузу только async-функцию, а не всю программу целиком. Event loop продолжает крутиться, а countdown просто возобновится позже, когда очередной промис из wait зарезолвится.
Через await можно ждать что угодно промисоподобное. Даже await 42 — вполне рабочий код: всё, что не является промисом, оборачивается в Promise.resolve(42) и тут же резолвится.
Обработка ошибок через try/catch в async/await
С обычными промисами ошибки ловят через .catch(). А в случае с async/await отклонённый промис превращается в обычное исключение, которое ловится привычным способом:
Один блок try/catch ловит всё, что случилось внутри с любым await. Сетевые сбои, ошибки парсинга JSON, ваши собственные throw — всё прилетит в один и тот же catch. По сравнению с вложенными цепочками .then/.catch это просто глоток свежего воздуха.
Есть один нюанс: fetch реджектится только на сетевых ошибках, а HTTP-статусы 4xx/5xx он считает нормальным ответом. Поэтому res.ok придётся проверять руками и кидать ошибку самостоятельно — этот паттерн вы будете встречать в боевом коде постоянно.
Не ставьте await в цикле, если без него можно обойтись
Это самая частая ошибка при работе с async/await в JavaScript. Последовательный await внутри цикла означает, что каждая итерация ждёт завершения предыдущей:
sequential отрабатывает примерно за 900 мс, parallel — около 300 мс. Правило простое: если задачи не зависят от результатов друг друга, запускайте их все сразу и делайте await Promise.all. Поштучный await нужен только тогда, когда следующему вызову реально требуется результат предыдущего.
Для коллекций идиома такая: Promise.all(items.map(async (x) => ...)). Обычный for...of с await внутри работает последовательно — иногда это как раз то, что нужно (rate-limiting, сохранение порядка), но чаще всего нет.
Совмещаем async/await и обычные промисы
Выбирать что-то одно не обязательно. async-функции возвращают промисы, а await работает с любым промисом, так что их спокойно можно смешивать:
Эти два стиля взаимозаменяемы. Используйте await, когда код лучше читается сверху вниз, и .then, если нужно быстро что-то сделать по ходу дела или вы работаете вне async-контекста.
Top-level await в ES-модулях
Раньше await можно было писать только внутри async-функции — на верхнем уровне скрипта он был запрещён. Теперь всё изменилось: внутри ES-модуля (файл .mjs или <script type="module">) await можно использовать прямо на верхнем уровне:
// в ES-модуле
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);
await на верхнем уровне откладывает завершение модуля до тех пор, пока ожидаемый промис не разрешится — и все, кто импортирует этот модуль, тоже будут ждать. Удобно для загрузки конфигов и динамических импортов, но злоупотреблять не стоит: один медленный top-level await тормозит всех, кто подтягивает этот модуль.
В файлах CommonJS и в обычных инлайновых скриптах такое по-прежнему падает с SyntaxError. Классический обходной путь — самовызывающаяся async-функция:
Типичные грабли, на которые все наступают
Короткий обзор самых частых проблем:
- Забыли
async.awaitвнутри обычной функции — это синтаксическая ошибка. Лечится либо добавлениемasync, либо вызовом асинхронной функции-обёртки через.then. - Забыли
awaitу результата. Записьconst data = getJSON(url);вернёт вам промис, а не данные. Если попробовать использовать это как значение, в выводе вы увидите[object Promise]. - Необработанные отклонения. Асинхронная функция, запущенная «вслепую» (
doWork();), молча проглотит ошибки, если вы не навесите.catchили не обернёте вызов вtry/catchсawait. forEachс асинхронным коллбэком. Конструкцияarray.forEach(async (x) => await something(x))на самом деле ничего не ждёт —forEachпросто игнорирует возвращённые промисы. Используйтеfor...ofсawaitлибоPromise.all(array.map(...)).
Запустите — "завершено?" выведется раньше любого "готово", потому что broken возвращает управление, не дожидаясь ничего. А fixed дожидается всех операций и только потом печатает "завершено!".
Когда стоит использовать async/await
По умолчанию выбирайте async/await для любого кода, где асинхронные шаги идут друг за другом, или там, где удобна обработка ошибок через try/catch. С «голыми» промисами стоит остаться для простых однострочников, для библиотечного кода, который просто возвращает промис и сам ничего не ждёт, либо когда действительно нужны комбинаторы вроде Promise.race или .finally() в цепочке.
Если пользоваться с умом, async/await превращает асинхронный код в рецепт: сделай это, потом это, потом вот это. Event loop никуда не делся — просто вам больше не нужно мыслить коллбэками.
Дальше: Fetch API
Почти во всех примерах fetch выступал просто как «какая-то асинхронная штука». Но он заслуживает отдельного разговора: как устроены запросы и ответы, как работать с JSON, как задавать заголовки и почему fetch не реджектится на HTTP-ошибках. Об этом — на следующей странице.
Часто задаваемые вопросы
Что вообще делают async/await в JavaScript?
async/await — это синтаксический сахар над промисами, который позволяет писать асинхронный код так, будто он синхронный. async помечает функцию как возвращающую промис, а await внутри такой функции ставит выполнение на паузу, пока промис не разрешится, и возвращает его значение. Под капотом это всё те же промисы — просто читать стало гораздо проще.
Можно ли использовать await вне async-функции?
На верхнем уровне ES-модуля — да, это называется top-level await. А вот в обычных функциях или в CommonJS-скриптах так не получится: await вне async — это синтаксическая ошибка. Решение простое: либо оберните код в async-функцию и вызовите её, либо переведите файл на ES-модули.
Как ловить ошибки с async/await?
Заверните вызовы с await в try/catch. Любой отклонённый (rejected) промис, который вы ожидаете через await, превратится в обычное исключение, и его подхватит catch. Для фоновых задач, которые вы не ожидаете через await, вешайте .catch() на сам промис — иначе получите unhandled rejection.
А await не блокирует всю программу?
Нет. await ставит на паузу только текущую async-функцию. Event loop при этом продолжает крутиться: таймеры срабатывают, другие асинхронные задачи выполняются, интерфейс остаётся отзывчивым. Вызывающий код сразу получает pending-промис и идёт дальше.