Если вы когда-нибудь писали на JavaScript и сталкивались с необходимостью дождаться ответа от сервера, то наверняка знаете, как это бывает: код прыгает, колбэки вложены друг в друга, и в итоге получается «адская пирамида». Но есть способ сделать асинхронность почти такой же простой, как синхронный PHP. Знакомьтесь — async/await.

Digital-студия WNDER

В этой статье я, PHP-разработчик с десятилетним стажем, объясню, как работает async/await в JavaScript, на пальцах и с примерами. Вы узнаете, как избавиться от callback hell, как обрабатывать ошибки и какие подводные камни вас ждут. Поехали!

Что такое асинхронность и почему это больно

В PHP всё просто: код выполняется сверху вниз, построчно. Запрос к базе — ждём ответ, потом идём дальше. В JavaScript всё иначе — он однопоточный, но не блокирующий. Это значит, что если нужно сделать запрос к серверу, JavaScript не будет сидеть и ждать, как PHP, а продолжит выполнять следующий код. А когда ответ придёт — вернётся и обработает.

Раньше для этого использовали колбэки — функции, которые вызываются после завершения операции. Но когда таких операций несколько, они вкладываются друг в друга, и получается вот такая каша:

getUser(function(user) {
    getPosts(user.id, function(posts) {
        getComments(posts[0].id, function(comments) {
            console.log(comments);
        });
    });
});

Согласитесь, читать такое — то ещё удовольствие. Это и есть «адская пирамида» или callback hell. Потом придумали промисы (Promises) — стало лучше, но всё равно много лишнего кода. А потом появились async/await — и всё стало почти как в PHP.

async/await: магия, которая делает код плоским

async/await — это синтаксический сахар над промисами. Вы просто пишете функцию с ключевым словом async, и внутри неё можно использовать await перед операцией, которая возвращает промис. JavaScript дождётся выполнения этой операции и только потом пойдёт дальше. Выглядит это так:

async function loadData() {
    const user = await getUser();
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    console.log(comments);
}

Красота, правда? Код стал плоским, читается как обычный синхронный. Но под капотом всё равно асинхронность — JavaScript не блокируется, просто мы говорим ему: «Подожди здесь, пока не придёт ответ, но не замораживай всю страницу».

Важно понимать: await можно использовать только внутри async-функций. Если попробуете вне — будет ошибка. Это как в PHP нельзя вызвать yield вне генератора.

Обработка ошибок: try/catch спешит на помощь

В PHP вы привыкли к блокам try/catch для исключений. В async/await всё точно так же. Если какой-то промис завершился с ошибкой, она выбросится как исключение, и вы её поймаете. Смотрите:

async function loadData() {
    try {
        const user = await getUser();
        const posts = await getPosts(user.id);
        console.log(posts);
    } catch (error) {
        console.error('Что-то пошло не так:', error);
    }
}

Если getUser() вернёт ошибку, она будет перехвачена в catch. Не нужно никаких .catch() у каждого промиса — всё в одном месте. Это сильно упрощает жизнь.

Но есть нюанс: если у вас несколько await подряд, и один из них упал, выполнение остановится. Иногда это нежелательно. Тогда можно использовать Promise.allSettled() или обрабатывать каждый промис отдельно, но это уже тема для отдельной статьи.

Параллельное выполнение: когда не нужно ждать по очереди

В примерах выше мы ждали каждый промис по очереди. Но часто бывает, что запросы не зависят друг от друга. Например, нужно получить данные о пользователе и список его заказов — эти запросы можно выполнять одновременно. Если вы напишете:

const user = await getUser();
const orders = await getOrders();

То второй запрос начнётся только после того, как завершится первый. Это медленно. Чтобы запустить их параллельно, используйте Promise.all():

const [user, orders] = await Promise.all([getUser(), getOrders()]);

Теперь оба запроса выполняются одновременно, и мы ждём, пока оба завершатся. Это как в PHP вы бы запустили два curl-запроса через curl_multi — только проще.

Если один из промисов упадёт, Promise.all() тоже упадёт. Если нужно, чтобы каждый выполнялся независимо, используйте Promise.allSettled() — он вернёт результаты всех, даже если некоторые с ошибками.

Практические советы и подводные камни

Как и в любом деле, здесь есть свои хитрости. Вот несколько советов, которые я выработал за годы практики.

  • Не забывайте про async в обработчиках событий. Если вы вешаете обработчик на клик, и внутри используете await, то функция должна быть async. Иначе получите ошибку.
  • Await в циклах — зло. Если вы пишете for (let i = 0; i < arr.length; i++) { await something(arr[i]); }, то это выполняется последовательно. Если можно параллельно — используйте Promise.all с map().
  • Не блокируйте главный поток. async/await не делает JavaScript многопоточным. Если у вас тяжёлые вычисления, они всё равно заблокируют интерфейс. Для тяжёлых задач используйте Web Workers.
  • Возвращайте промисы из async-функций. Async-функция всегда возвращает промис. Если вы что-то возвращаете, оно оборачивается в Promise.resolve(). Это удобно, но помните: если не дождаться, получите промис вместо значения.
Лайфхак: Если вам нужно выполнить несколько независимых запросов, но вы хотите дождаться хотя бы одного — используйте Promise.race(). Он вернёт результат самого быстрого запроса. Это полезно для таймаутов или загрузки критичных данных.

Теперь давайте сравним, как выглядит асинхронность в PHP и в JavaScript. Многие PHP-разработчики привыкли к синхронности, но современный PHP тоже поддерживает асинхронность через Swoole или ReactPHP. Однако это редкость. В JS это стандарт.

Аспект PHP (синхронный) JavaScript (async/await)
Выполнение Строго последовательное Последовательное с ожиданием
Обработка ошибок try/catch try/catch (в async)
Параллельность Требует многопоточности Promise.all() и др.
Читаемость Хорошая Отличная (после привыкания)

Что в итоге

async/await — это мощный инструмент, который делает асинхронный код почти таким же понятным, как синхронный. Вы избавляетесь от колбэков, легко обрабатываете ошибки и можете запускать запросы параллельно. Главное — помнить о подводных камнях: не использовать await в циклах без необходимости, не забывать про async в обработчиках событий и не блокировать главный поток.

Если вы PHP-разработчик и только начинаете с JavaScript, не бойтесь — async/await максимально дружелюбен. Попробуйте переписать свой старый код с колбэками на async/await, и вы почувствуете разницу. Удачи!

Студия WNDER