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

Digital-студия WNDER

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

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

Зачем это нужно? Чтобы код было легче читать и поддерживать. Сравните два подхода.

// Было: цепочка промисов
fetch('/user')
  .then(response => response.json())
  .then(user => fetch(`/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

// Стало: async/await
async function loadPosts() {
  try {
    const userResponse = await fetch('/user');
    const user = await userResponse.json();
    const postsResponse = await fetch(`/posts/${user.id}`);
    const posts = await postsResponse.json();
    console.log(posts);
  } catch (error) {
    console.error(error);
  }
}
loadPosts();

Второй вариант читается сверху вниз, как обычный рассказ. Никаких вложенных then и непонятных catch. Это и есть главная ценность async/await.

Как это работает под капотом

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

Вот простой пример:

async function getNumber() {
  return 42;
}

getNumber().then(value => console.log(value)); // 42

async function waitAndPrint() {
  console.log('Начало');
  const result = await new Promise(resolve => setTimeout(() => resolve('Привет'), 1000));
  console.log(result);
  console.log('Конец');
}
waitAndPrint();
// Выведет: Начало, затем через секунду: Привет, Конец

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

Типичные ошибки и как их избежать

Даже с async/await можно наступить на грабли. Вот самые частые.

  • Забыли await. Функция вернёт промис, а вы будете думать, что это уже результат. Всегда проверяйте, где нужен await.
  • Используете await в цикле forEach. forEach не умеет ждать. Используйте for...of или Promise.all.
  • Не обрабатываете ошибки. Без try/catch отклонённый промис приведёт к необработанному исключению.
  • Последовательные await там, где можно параллельно. Если два запроса независимы, запускайте их одновременно через Promise.all.
Правило: Если два асинхронных вызова не зависят друг от друга — не ждите их по очереди, запускайте параллельно. Это сэкономит время.

Пример параллельного выполнения:

// Плохо: последовательно
async function sequential() {
  const a = await fetch('/a');
  const b = await fetch('/b');
  return [a, b];
}

// Хорошо: параллельно
async function parallel() {
  const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
  return [a, b];
}

Сравнение подходов: колбэки, промисы, async/await

Чтобы понять преимущества, полезно сравнить три стиля. В таблице ниже — краткая характеристика.

Подход Читаемость Обработка ошибок Гибкость
Колбэки Низкая (callback hell) Сложно (нужно передавать ошибку в каждый колбэк) Средняя
Промисы Средняя (цепочки then) Удобно (catch в конце) Высокая
Async/await Высокая (как синхронный) Удобно (try/catch) Высокая

Как видите, async/await выигрывает по читаемости и обработке ошибок, но не отменяет промисы — они работают под капотом.

Практические советы и лайфхаки

Вот несколько приёмов, которые сделают ваш код чище.

  • Всегда оборачивайте await в try/catch, если не уверены, что промис не отклонится.
  • Используйте async/await в обработчиках событий и роутах — это упрощает логику.
  • Для параллельных запросов применяйте Promise.all, а если нужно дождаться всех, даже с ошибками — Promise.allSettled.
  • Не делайте функцию async, если внутри нет await — это лишнее.
  • Помните, что await можно использовать только внутри async-функции (или на верхнем уровне модуля в современных средах).
Лайфхак: Если нужно выполнить несколько асинхронных операций и дождаться каждой, но не хотите останавливать всю функцию при первой ошибке, используйте Promise.allSettled — он вернёт массив результатов с статусами.

Пример с allSettled:

async function loadAll() {
  const results = await Promise.allSettled([
    fetch('/a'),
    fetch('/b'),
    fetch('/c')
  ]);
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Запрос ${index} успешен:`, result.value);
    } else {
      console.error(`Запрос ${index} упал:`, result.reason);
    }
  });
}

Что в итоге

Async/await — это не просто модный синтаксис, а реальный инструмент для упрощения асинхронного кода. Он делает код понятнее, облегчает отладку и уменьшает количество ошибок. Главное — не забывать про параллельное выполнение и обработку исключений. Начните использовать async/await в своём проекте, и вы почувствуете разницу. А если что-то пойдёт не так — вспомните про try/catch и Promise.all. Удачи в кодинге!

Студия WNDER