ПроКодинг - Откроем для вас мир IT!

Когда вы пишете код, который ждёт ответа от сервера, ваш мозг автоматически представляет себе паузу. Но браузер не ставит всё на паузу - он продолжает рисовать интерфейс и реагировать на клики. Именно здесь async/await is синтаксический сахар над промисами, позволяющий писать асинхронный код в синхронном стиле без блокировки главного потока. Введённые в ES2017, эти операторы изменили то, как мы работаем с JavaScript, устранив «ад колбэков» и сделав логику чтения более линейной.

Почему обычный синхронный код не работает

Представьте, что вы заказываете пиццу. Если бы вы стояли у телефона и ждали, пока курьер приедет, вы бы потеряли время. В программировании это называется блокировкой. Если функция тратит 2 секунды на запрос данных, а код ниже неё должен выполняться сразу, синхронный подход зависнет. Event Loop в браузере следит за тем, чтобы главный поток оставался свободным для обработки событий пользователя. Асинхронные функции позволяют коду «отойти в сторону», пока идёт ожидание, и вернуться, когда данные готовы.

Как устроен механизм async/await

Ключевое слово async перед функцией гарантирует, что она всегда вернёт Promise. Даже если внутри нет никаких операций ожидания, результат будет обёрнут в промис со статусом "fulfilled". Оператор await можно использовать только внутри таких функций (или на верхнем уровне модулей). Он останавливает выполнение текущей функции до тех пор, пока промис не разрешится. Важно понимать: он блокирует только функцию, в которой находится, а не весь скрипт.

  • Функция помечается как async: теперь её вызов возвращает Promise.
  • Внутри используется await: код ниже строки с await не выполняется, пока промис не завершится.
  • Главный поток свободен: другие задачи, таймеры и события обрабатываются нормально.

Сравнение стилей: Колбэки vs Промисы vs Async/Await

Чтобы оценить пользу, давайте посмотрим на три способа получить данные из API. Разница в читаемости становится очевидной уже после третьего уровня вложенности.

Сравнение подходов к асинхронному коду в JavaScript
ХарактеристикаКолбэкиПромисы (.then)Async/Await
ЧитаемостьНизкая (пирамида ужаса)СредняяВысокая (похож на sync)
Обработка ошибокОтдельный параметр errMethode .catch()Try/Catch
Поддержка break/continueНетНетДа
ПроизводительностьМаксимальнаяВысокаяВысокая (чуть медленнее из-за генераторов)
Abstract comparison of tangled cables, linked chains, and a smooth ribbon representing different JS patterns

Практический пример: Загрузка профиля пользователя

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

Типичные ошибки новичков

Даже опытные разработчики иногда спотыкаются о тонкости асинхронного выполнения. Вот три самых частых ловушки:

  1. Ожидание чего-то, что не является промисом. Если вы напишете await 5, код просто продолжится на следующей строке. Это не ошибка, но может ввести в заблуждение при отладке.
  2. Использование await вне async-функции. До появления top-level await это вызывало SyntaxError. Теперь в модулях это разрешено, но в обычных скриптах всё ещё нельзя.
  3. Последовательное выполнение независимых задач. Если вам нужно скачать два файла, которые не зависят друг от друга, не стоит ждать первый, а потом второй. Используйте Promise.all.
Two streams of light merging into one, visualizing parallel execution of asynchronous requests

Параллельное выполнение с 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() может быть короче.
  • Вы оптимизируете производительность на микроуровне в критических участках кода (разница минимальна, но существует).
Для 99% веб-приложений сегодня async/await - это стандарт де-факто. Он делает код предсказуемым, легко тестируемым и понятным для новых членов команды.

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