JavaScript — однопоточный язык. Это значит, что он может выполнять только одну операцию за раз. Но как тогда работают запросы к серверу, таймеры и чтение файлов? Всё дело в асинхронности. Раньше приходилось использовать колбэки, потом появились промисы, а сегодня есть async/await — синтаксический сахар, который делает асинхронный код похожим на обычный синхронный. Если вы до сих пор путаетесь в then и catch, эта статья для вас.

Digital-студия WNDER

Что такое асинхронность и зачем она нужна

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

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

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

Синтаксис async и await: первый взгляд

Ключевое слово async ставится перед функцией. Оно говорит: «Эта функция всегда возвращает промис». Даже если вы вернёте просто число, оно обернётся в промис.

Ключевое слово await можно использовать только внутри async-функции. Оно заставляет JavaScript ждать, пока промис выполнится, и возвращает его результат. При этом код выглядит как синхронный.

// Обычная функция
function getData() {
  return fetch('https://api.example.com/data');
}

// Асинхронная функция
async function getDataAsync() {
  return fetch('https://api.example.com/data');
}

// Использование
async function main() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
}

main();

Обратите внимание: мы не используем .then(). Вместо этого просто пишем await и получаем результат. Код читается сверху вниз, как обычный синхронный.

Правило: Если функция содержит хотя бы один await, она обязана быть объявлена как async. Иначе будет синтаксическая ошибка.

Обработка ошибок: try/catch вместо catch

С промисами мы использовали .catch() в конце цепочки. С async/await всё проще: обычный try...catch, как в синхронном коде.

async function fetchUser(id) {
  try {
    const response = await fetch(`https://api.example.com/users/${id}`);
    if (!response.ok) {
      throw new Error('Пользователь не найден');
    }
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Ошибка:', error.message);
    // Можно вернуть запасной вариант или пробросить ошибку дальше
    throw error;
  }
}

Важно: await выбрасывает исключение, если промис отклонён. Поэтому try...catch ловит и сетевые ошибки, и ошибки в логике.

Последовательное и параллельное выполнение

Одна из главных ошибок новичков — запускать несколько независимых промисов последовательно, когда их можно запустить параллельно. Это замедляет программу.

Сравните два подхода:

// Плохо: ждём по очереди
async function sequential() {
  const user = await fetch('/user');
  const posts = await fetch('/posts');
  const comments = await fetch('/comments');
  return { user, posts, comments };
}

// Хорошо: запускаем все сразу
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetch('/user'),
    fetch('/posts'),
    fetch('/comments')
  ]);
  return { user, posts, comments };
}

Во втором случае все три запроса уйдут на сервер одновременно. Время выполнения сократится в три раза (если запросы независимы).

Лайфхак: Используйте Promise.all(), когда результаты промисов не зависят друг от друга. Если хотя бы один промис отклонится, Promise.all сразу выбросит ошибку. Для более гибкой обработки есть Promise.allSettled().

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

Чтобы понять преимущества async/await, посмотрим на таблицу.

Подход Читаемость Обработка ошибок Отладка
Колбэки Низкая (ад пирамид) Сложная, легко пропустить Трудно ставить точки останова
Промисы Средняя, цепочки then Через .catch() Лучше, но цепочки мешают
async/await Высокая, как синхронный код try/catch, как обычно Отлично, точки останова работают

Как видите, async/await выигрывает по всем параметрам. Неудивительно, что это стандарт современного JavaScript.

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

Вот несколько вещей, которые стоит запомнить, чтобы не наступить на грабли.

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

Пример из реальной жизни: загрузка данных пользователя

Допустим, нам нужно загрузить профиль пользователя, затем его посты, а потом комментарии к первому посту. Всё это — асинхронные запросы.

async function loadUserProfile(userId) {
  try {
    // Загружаем профиль
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();

    // Загружаем посты пользователя
    const postsResponse = await fetch(`/api/users/${userId}/posts`);
    const posts = await postsResponse.json();

    // Если есть посты, загружаем комментарии к первому
    let comments = [];
    if (posts.length > 0) {
      const commentsResponse = await fetch(`/api/posts/${posts[0].id}/comments`);
      comments = await commentsResponse.json();
    }

    return { user, posts, comments };
  } catch (error) {
    console.error('Не удалось загрузить профиль:', error);
    throw error;
  }
}

// Использование
loadUserProfile(123)
  .then(data => console.log(data))
  .catch(err => console.error(err));

Код читается сверху вниз, как обычный сценарий. Мы точно знаем, в каком порядке выполняются запросы, и легко можем добавить логику между ними.

Что в итоге

Async/await — это не просто синтаксический сахар. Это способ писать асинхронный код, который легко читать, отлаживать и поддерживать. Он не заменяет промисы, а работает поверх них. Вы по-прежнему используете Promise.all, Promise.race и другие методы, но теперь без громоздких цепочек .then().

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

Асинхронность в JavaScript перестала быть болью. Пользуйтесь.

Студия WNDER