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

Что такое async/await и зачем это нужно

Представьте, что вы заказываете пиццу. В синхронном мире вы стоите у телефона и ждете, пока оператор примет заказ, потом ждете курьера, потом едите. Всё последовательно. В асинхронном мире вы можете заниматься своими делами, пока пицца готовится. JavaScript изначально асинхронный: он не блокирует выполнение кода, когда ждет ответа от сервера или чтения файла.

Раньше для этого использовали колбэки (функции обратного вызова), но это приводило к «аду колбэков» — когда один колбэк вложен в другой, и код становится нечитаемым. Потом пришли промисы (объекты, представляющие результат асинхронной операции), но всё равно нужно было писать цепочки .then(). И вот async/await — это синтаксический сахар, который делает работу с промисами такой же простой, как с обычным кодом.

Правило: Используйте async/await только внутри функций, объявленных с ключевым словом async. Это обязательно, иначе await не сработает.

Как работает async и await: разбор на примере

Давайте начнем с простого. Допустим, у нас есть функция, которая имитирует запрос к серверу и возвращает данные через 2 секунды. Напишем её с помощью промиса:

function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: 'Товар' });
    }, 2000);
  });
}

Теперь вызовем её с async/await:

async function fetchData() {
  console.log('Начинаем запрос...');
  const data = await getData();
  console.log('Данные получены:', data);
}

fetchData();

Что здесь происходит? Ключевое слово await говорит JavaScript: «Подожди, пока промис не завершится, и только потом продолжай выполнение». При этом выполнение самой функции приостанавливается, но другие части программы (например, обработка кликов) работают дальше. Это не блокирует весь скрипт.

Обработка ошибок: try...catch в действии

В асинхронном коде ошибки могут возникать, как и в любом другом. С async/await мы используем старый добрый try...catch, который знаком каждому PHP-разработчику. Это гораздо удобнее, чем ловить ошибки через .catch() у промисов.

Пример с ошибкой:

function getDataWithError() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('Сервер не отвечает'));
    }, 1000);
  });
}

async function fetchDataSafe() {
  try {
    const data = await getDataWithError();
    console.log(data);
  } catch (error) {
    console.error('Ошибка:', error.message);
  }
}

fetchDataSafe();

Как видите, код остаётся линейным и читаемым. Если бы мы использовали промисы, пришлось бы писать цепочку .catch().

Лайфхак: Всегда оборачивайте вызовы с await в try...catch. Это спасёт вас от неожиданных падений приложения. Даже если вы уверены, что ошибки не будет, лучше перестраховаться.

Параллельное выполнение: Promise.all и async/await

Иногда нужно запустить несколько асинхронных операций одновременно и дождаться их всех. Например, получить данные о пользователе и список его заказов. Если использовать await по очереди, то будет медленно. Решение — Promise.all.

Пример:

function getUser() {
  return new Promise(resolve => setTimeout(() => resolve({ name: 'Иван' }), 1000));
}

function getOrders() {
  return new Promise(resolve => setTimeout(() => resolve(['Заказ1', 'Заказ2']), 1500));
}

async function loadDashboard() {
  try {
    const [user, orders] = await Promise.all([getUser(), getOrders()]);
    console.log('Пользователь:', user);
    console.log('Заказы:', orders);
  } catch (error) {
    console.error('Ошибка загрузки:', error);
  }
}

loadDashboard();

Promise.all принимает массив промисов и возвращает один промис, который завершается, когда завершаются все переданные. Если хотя бы один промис завершится ошибкой, то весь Promise.all завершится с этой ошибкой.

МетодОписаниеКогда использовать
Promise.allЖдёт завершения всех промисов. Если один упадёт, то весь промис упадёт.Когда все операции обязательны для успеха.
Promise.allSettledЖдёт завершения всех промисов, неважно, с ошибкой или успехом.Когда нужно дождаться всех, даже если некоторые упадут.
Promise.raceЖдёт первый завершённый промис (успех или ошибка).Для таймаутов или когда нужен самый быстрый ответ.

Асинхронные функции в циклах: подводные камни

Многие новички делают ошибку, используя await внутри обычного forEach. forEach не ждёт завершения асинхронной функции, поэтому код выполняется неправильно. Вместо этого используйте for...of или Promise.all с маппингом.

Пример неправильного кода:

const ids = [1, 2, 3];
ids.forEach(async (id) => {
  const data = await fetchData(id);
  console.log(data);
});
console.log('Цикл завершён'); // Выполнится раньше, чем данные придут

Правильный вариант с for...of:

async function processIds(ids) {
  for (const id of ids) {
    const data = await fetchData(id);
    console.log(data);
  }
}

processIds([1, 2, 3]);

Или, если порядок не важен, используйте Promise.all:

async function processIdsParallel(ids) {
  const promises = ids.map(id => fetchData(id));
  const results = await Promise.all(promises);
  console.log(results);
}

Выбор зависит от задачи: если нужно последовательно обрабатывать каждый элемент (например, записывать в базу по одному), то for...of. Если можно параллельно — Promise.all.

Сравнение с PHP: что общего и в чём разница

В PHP тоже есть async/await, но там он работает через библиотеки (например, ReactPHP) или с помощью генераторов. В PHP 8.1 появились fibers, но они пока не так популярны. В JavaScript async/await — это встроенная возможность, и она используется повсеместно.

ПараметрJavaScriptPHP
Встроенная поддержкаДа, с ES2017Нет нативно, только через библиотеки
Модель выполненияОднопоточная, событийнаяМногопоточная (с расширениями)
Обработка ошибокtry...catchtry...catch (в библиотеках)
Параллельное выполнениеPromise.all, Promise.raceЧерез многопоточность или корутины

Итоговое резюме: что в итоге

Async/await — это мощный инструмент, который делает асинхронный код читаемым и понятным. Он не меняет суть асинхронности, но упрощает её восприятие. Главные правила, которые нужно запомнить:

  • Функция с await должна быть объявлена как async.
  • Всегда используйте try...catch для обработки ошибок.
  • Для параллельных задач используйте Promise.all, а не последовательные await.
  • В циклах отдавайте предпочтение for...of или Promise.all с map.

Теперь вы готовы использовать async/await в своих проектах. Начните с малого: замените цепочки .then() на async/await в одном из скриптов, и вы почувствуете разницу. Удачи в кодинге!

Студия WNDER