В предыдущей главе мы написали fetch через .then(). Это работает, но если запросов становится больше, код быстро превращается в лесенку из вложенных колбэков. Сравните — и почувствуйте разницу.

Старый синтаксис: .then().catch()

fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(post => {
    return fetch(`https://jsonplaceholder.typicode.com/users/${post.userId}`);
  })
  .then(response => response.json())
  .then(user => {
    console.log(`Пост "${post.title}" написал ${user.name}`);
  })
  .catch(error => console.error(error));

Здесь два последовательных запроса: сначала тянем пост, потом по его userId тянем автора. Уже на двух запросах появляется проблема: переменная post в последнем .then() недоступна — её область видимости закончилась двумя ступенями выше. Приходится либо выносить во внешний скоуп, либо вкладывать .then() друг в друга — собственно, та самая лесенка.

Новый синтаксис: async/await

Тот же код через async/await:

async function loadPostWithAuthor() {
  try {
    const postResponse = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    if (!postResponse.ok) throw new Error(`HTTP ${postResponse.status}`);
    const post = await postResponse.json();

    const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${post.userId}`);
    if (!userResponse.ok) throw new Error(`HTTP ${userResponse.status}`);
    const user = await userResponse.json();

    console.log(`Пост "${post.title}" написал ${user.name}`);
  } catch (error) {
    console.error(error);
  }
}

loadPostWithAuthor();

Те же действия, но код читается сверху вниз, как обычный синхронный. Переменная post доступна до конца функции. Ошибки ловятся через привычный try/catch.

Правила игры

Чтобы это работало, нужно знать две вещи.

1. await можно ставить только внутри async-функции. Если попробовать использовать await в обычной функции — синтаксическая ошибка. В современных модулях ES2022+ есть top-level await (на уровне модуля без обёртки функцией), но в скриптах в браузере чаще всего нужна обёртка.

// Так нельзя — будет SyntaxError:
function bad() {
  const r = await fetch('/'); // ошибка
}

// Так правильно:
async function good() {
  const r = await fetch('/');
}

2. async-функция всегда возвращает Promise. Даже если внутри написать return 42, наружу прилетит Promise<42>. Это значит, что вызов async-функции, к которой не приставили .then() или await, — запускает её, но результат не дождётся.

Ошибки через try/catch

Главное преимущество async/await для новичка — знакомый try/catch. В нём ловятся:

  • сетевые ошибки от fetch (нет интернета, DNS не разрешился);
  • любые throw, которые мы делаем сами после проверки response.ok;
  • ошибки парсинга в response.json() — например, если тело не JSON.

То же самое с .catch() делается, но синтаксис громоздче. Подробный разбор — в модуле 6.

Параллельные запросы: Promise.all

Один нюанс. Если запросы не зависят друг от друга — ставить их последовательно через await неэффективно. Каждый ждёт предыдущий, общее время — сумма всех.

// Долго: запросы идут один за другим
const a = await fetch('/api/a').then(r => r.json());
const b = await fetch('/api/b').then(r => r.json());
const c = await fetch('/api/c').then(r => r.json());

Если друг от друга они не зависят — запускаем параллельно:

// Быстро: запросы летят одновременно
const [a, b, c] = await Promise.all([
  fetch('/api/a').then(r => r.json()),
  fetch('/api/b').then(r => r.json()),
  fetch('/api/c').then(r => r.json()),
]);

Promise.all принимает массив Promise-ов и возвращает один общий Promise, который разрешится, когда разрешатся все. Если хотя бы один упадёт — общий тоже упадёт. Если ошибки нужно ловить по отдельности, есть Promise.allSettled — он дождётся всех, и в результате каждого будет либо {status: 'fulfilled', value}, либо {status: 'rejected', reason}.

Сравним два стиля бок о бок

Что использовать в проекте

В современных кодовых базах async/await — стандарт. .then-цепочки встречаются в местах, где нужны короткие однострочники, или в утилитарном коде, который не выгодно оборачивать в async-функцию. Знать надо обе формы: при чтении чужого кода обе встречаются.

В этом курсе дальше используем async/await как основной стиль.