Когда вы пишете код, который ждёт ответа от сервера, ваш мозг автоматически представляет себе паузу. Но браузер не ставит всё на паузу - он продолжает рисовать интерфейс и реагировать на клики. Именно здесь async/await is синтаксический сахар над промисами, позволяющий писать асинхронный код в синхронном стиле без блокировки главного потока. Введённые в ES2017, эти операторы изменили то, как мы работаем с JavaScript, устранив «ад колбэков» и сделав логику чтения более линейной.
Почему обычный синхронный код не работает
Представьте, что вы заказываете пиццу. Если бы вы стояли у телефона и ждали, пока курьер приедет, вы бы потеряли время. В программировании это называется блокировкой. Если функция тратит 2 секунды на запрос данных, а код ниже неё должен выполняться сразу, синхронный подход зависнет. Event Loop в браузере следит за тем, чтобы главный поток оставался свободным для обработки событий пользователя. Асинхронные функции позволяют коду «отойти в сторону», пока идёт ожидание, и вернуться, когда данные готовы.
Как устроен механизм async/await
Ключевое слово async перед функцией гарантирует, что она всегда вернёт Promise. Даже если внутри нет никаких операций ожидания, результат будет обёрнут в промис со статусом "fulfilled". Оператор await можно использовать только внутри таких функций (или на верхнем уровне модулей). Он останавливает выполнение текущей функции до тех пор, пока промис не разрешится. Важно понимать: он блокирует только функцию, в которой находится, а не весь скрипт.
- Функция помечается как async: теперь её вызов возвращает Promise.
- Внутри используется await: код ниже строки с await не выполняется, пока промис не завершится.
- Главный поток свободен: другие задачи, таймеры и события обрабатываются нормально.
Сравнение стилей: Колбэки vs Промисы vs Async/Await
Чтобы оценить пользу, давайте посмотрим на три способа получить данные из API. Разница в читаемости становится очевидной уже после третьего уровня вложенности.
| Характеристика | Колбэки | Промисы (.then) | Async/Await |
|---|---|---|---|
| Читаемость | Низкая (пирамида ужаса) | Средняя | Высокая (похож на sync) |
| Обработка ошибок | Отдельный параметр err | Methode .catch() | Try/Catch |
| Поддержка break/continue | Нет | Нет | Да |
| Производительность | Максимальная | Высокая | Высокая (чуть медленнее из-за генераторов) |
Практический пример: Загрузка профиля пользователя
Допустим, нам нужно получить список заказов, а затем для каждого заказа - детали товара. Без async/await это было бы вложенным кошмаром. С ним логика выглядит последовательно.
async function loadUserOrders(userId) {
try {
// Ждём список заказов
const ordersResponse = await fetch(`/api/orders/${userId}`);
if (!ordersResponse.ok) throw new Error('Ошибка сети');
const orders = await ordersResponse.json();
// Цикл работает как в обычном коде
for (const order of orders) {
console.log(`Заказ ${order.id} получен`);
// Можно даже сделать паузу или сложную логику
await processOrderDetails(order.id);
}
return orders;
} catch (error) {
console.error('Не удалось загрузить заказы:', error.message);
throw error; // Пробрасываем дальше, если нужно
}
}
Здесь fetch является нативным методом, возвращающим промис. Метод json() также возвращает промис, поэтому его тоже можно ждать. Использование try...catch позволяет ловить ошибки любого типа: сетевые сбои, неверный формат JSON или исключения, которые мы бросили сами.
Типичные ошибки новичков
Даже опытные разработчики иногда спотыкаются о тонкости асинхронного выполнения. Вот три самых частых ловушки:
- Ожидание чего-то, что не является промисом. Если вы напишете
await 5, код просто продолжится на следующей строке. Это не ошибка, но может ввести в заблуждение при отладке. - Использование await вне async-функции. До появления top-level await это вызывало SyntaxError. Теперь в модулях это разрешено, но в обычных скриптах всё ещё нельзя.
- Последовательное выполнение независимых задач. Если вам нужно скачать два файла, которые не зависят друг от друга, не стоит ждать первый, а потом второй. Используйте
Promise.all.
Параллельное выполнение с Promise.all
Одна из главных причин, почему async/await часто критикуют, - ложное впечатление, что все операции идут последовательно. Но это не так. Вы можете запустить несколько промисов одновременно и ждать их завершения вместе.
async function getUserData() {
// Эти два запроса начнутся одновременно
const [profileRes, postsRes] = await Promise.all([
fetch('/api/profile'),
fetch('/api/posts')
]);
const profile = await profileRes.json();
const posts = await postsRes.json();
return { profile, posts };
}
Если один из промисов упадёт, Promise.all мгновенно отклонится. Если же вам нужен частичный успех (например, показать профиль, даже если посты не загрузились), используйте Promise.allSettled.
Работа с ошибками и таймаутами
Асинхронный код требует особого внимания к ошибкам. Общее правило: если вы используете await, обязательно оборачивайте код в try...catch. Иначе необработанные ошибки могут сломать приложение или остаться незамеченными в консоли.
fetch не имеет встроенного таймаута, поэтому его часто комбинируют с AbortController или сторонними библиотеками. Это предотвращает вечное зависание интерфейса, если сервер перестал отвечать.
Когда стоит избегать async/await
Несмотря на популярность, этот инструмент не панацея. Избегайте его, если:
- Вы работаете в очень старых браузерах без поддержки полифилов.
- Логика крайне простая и состоит из одного шага - обычный
.then()может быть короче. - Вы оптимизируете производительность на микроуровне в критических участках кода (разница минимальна, но существует).
Можно ли использовать await внутри цикла for?
Да, можно. При этом итерации будут выполняться последовательно: каждая следующая начнётся только после завершения предыдущей. Если порядок не важен, лучше собрать массив промисов и передать его в Promise.all для параллельного выполнения.
В чём разница между async/await и промисами?
Технически они эквивалентны: async/await компилируется в цепочки промисов или генераторы. Главное отличие - синтаксис. Async/await позволяет использовать привычные конструкции вроде try/catch и циклов, делая код похожим на синхронный, тогда как промисы требуют использования методов then и catch.
Блокирует ли await основной поток выполнения?
Нет. Await приостанавливает выполнение только текущей асинхронной функции. Основной поток (Event Loop) остается свободным для обработки других задач, рендеринга и событий пользователя. Это ключевое преимущество асинхронного подхода в JavaScript.
Что произойдет, если забыть поставить async перед функцией?
Если вы используете await внутри обычной функции (не async), вы получите SyntaxError. Однако на верхнем уровне модуля (ES Modules) использование await без объявления функции допустимо и называется top-level await.
Какой метод лучше использовать для нескольких независимых запросов: Promise.all или Promise.allSettled?
Promise.all подходит, когда все результаты критически важны, и ошибка любого одного запроса должна прервать процесс. Promise.allSettled лучше использовать, когда нужно получить статус всех запросов независимо от того, успешны они были или нет, чтобы обработать ошибки отдельно для каждого.