Навигация

Асинхронность: Promise, async/await

JavaScript — однопоточный язык, но большая часть реальных задач требует асинхронности: сетевые запросы, таймеры, работа с файлами, события пользователя. Современный способ работать с асинхронностью — Promise и синтаксический сахар async/await. Понимание их поведения, приоритетов в Event Loop и обработки ошибок — обязательно для собеседований и для написания стабильного кода.

Содержание

  1. Зачем нужна асинхронность
  2. Promise: состояния и жизненный цикл
  3. Promise и Event Loop
  4. Методы Promise: all, race, allSettled, any
  5. async/await
  6. Последовательное и параллельное выполнение
  7. Обработка ошибок
  8. Отмена асинхронных операций: AbortController
  9. Ключевые тезисы для интервью
  10. Заключение
  11. Полезные ссылки

Зачем нужна асинхронность

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

const data = fetchDataSync(); // блокирует на 1 секунду
console.log('Done');

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

До Promise асинхронность реализовывалась через вложенные callback’и:

getData((err, data) => {
  if (err) { /* ... */ }
  processData(data, (err, result) => {
    if (err) { /* ... */ }
    saveResult(result, (err) => {
      // callback hell
    });
  });
});

Promise и async/await заменили этот подход плоским, читаемым кодом.


Promise: состояния и жизненный цикл

Promise — это объект, представляющий результат асинхронной операции. У него три состояния:

  • pending — операция ещё не завершена.
  • fulfilled — операция успешно завершена, есть результат.
  • rejected — операция завершилась с ошибкой.

Состояние может измениться только один раз: из pending в fulfilled или из pending в rejected. После этого Promise становится settled.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Done');
  }, 1000);
});

promise
  .then((value) => console.log(value))   // Done
  .catch((error) => console.error(error))
  .finally(() => console.log('Finished'));

Методы Promise

  • .then(onFulfilled, onRejected) — обработка успеха или ошибки.
  • .catch(onRejected) — обработка ошибки (синоним .then(null, onRejected)).
  • .finally(onFinally) — выполняется в любом случае, не получает аргументов.
fetch('/api/user')
  .then((response) => response.json())
  .then((user) => console.log(user))
  .catch((error) => console.error('Request failed:', error))
  .finally(() => console.log('Cleanup'));

Цепочки then

Каждый .then возвращает новый Promise. Если callback возвращает значение, следующий .then получит его. Если возвращается Promise, цепочка дождётся его выполнения.

Promise.resolve(1)
  .then((x) => x + 1)          // 2
  .then((x) => Promise.resolve(x * 2)) // 4
  .then((x) => console.log(x)); // 4

Promise и Event Loop

Promise используют микрозадачи (microtasks). Callback из .then / .catch / .finally попадает в микрозадачную очередь, а не в очередь макрозадач.

console.log('Start');

setTimeout(() => console.log('Timeout'), 0);

Promise.resolve().then(() => console.log('Promise'));

console.log('End');
// Start
// End
// Promise
// Timeout

Почему Promise выполняется раньше setTimeout? Потому что после синхронного кода Event Loop опустошает все микрозадачи, и только потом берёт следующую макрозадачу.

Это важно для async/await: код после await тоже становится микрозадачей.

async function example() {
  console.log('Before await');
  await Promise.resolve();
  console.log('After await'); // микрозадача
}

example();
console.log('Sync');
// Before await
// Sync
// After await

Методы Promise: all, race, allSettled, any

Promise.all

Ожидает выполнения всех Promise. Возвращает массив результатов в том же порядке. Если хотя бы один Promise отклонён — весь Promise.all отклоняется.

const [users, posts] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
]);

Promise.race

Возвращает результат первого завершившегося Promise — успешного или отклонённого.

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timeout')), 5000)
);

const data = fetch('/api/data');

const result = await Promise.race([data, timeout]);

Promise.allSettled

Ожидает всех, но никогда не отклоняется. Возвращает массив объектов:

const results = await Promise.allSettled([
  Promise.resolve(1),
  Promise.reject('error'),
]);

// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 'error' }
// ]

Promise.any

Возвращает результат первого успешно выполнившегося Promise. Если все отклонены — возвращает AggregateError.

try {
  const fastest = await Promise.any([
    fetch('/mirror-1'),
    fetch('/mirror-2'),
    fetch('/mirror-3'),
  ]);
} catch (e) {
  console.log('All failed:', e.errors);
}

Сравнение

МетодУспехОшибкаКогда использовать
Promise.allВсе выполнилисьПервый отказПараллельные зависимые запросы
Promise.raceПервый результатПервый отказТаймауты
Promise.allSettledВсегдаНикогдаНужно узнать результат каждого
Promise.anyПервый успехВсе отказалиЗеркала, fallback’ы

async/await

async/await — синтаксический сахар над Promise. Функция, объявленная с async, всегда возвращает Promise.

async function getUser() {
  return { id: 1, name: 'Alice' };
}

getUser().then((user) => console.log(user));

await приостанавливает выполнение функции до завершения Promise. Он может использоваться только внутри async-функции (или на верхнем уровне модулей ES).

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

Важно: await не блокирует весь Event Loop, только текущую async-функцию. Остальной код продолжает выполняться.


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

Последовательное

async function sequential() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  return posts;
}

Запросы выполняются один за другим. Общее время — сумма задержек.

Параллельное

async function parallel() {
  const [user, posts] = await Promise.all([
    fetchUser(),
    fetchPosts(),
  ]);
  return { user, posts };
}

Запросы начинаются одновременно. Общее время — максимальная задержка.

Распространённая ошибка

async function wrong() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  // user и posts не зависят друг от друга, но выполняются последовательно
}

Если запросы независимы, используйте Promise.all.


Обработка ошибок

try/catch с await

async function loadData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to load data:', error.message);
    return null;
  }
}

.catch() для Promise

fetch('/api/data')
  .then((response) => response.json())
  .catch((error) => console.error(error));

Unhandled rejection

Если Promise отклонён, но нет обработчика ошибки, возникает unhandledrejection:

Promise.reject(new Error('Oops'));
// Uncaught (in promise) Error: Oops

Можно отловить глобально:

window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled rejection:', event.reason);
});

Важно: если ошибка обработана через .catch или try/catch, событие не возникает.

Обработка ошибок в Promise.all

try {
  const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
  ]);
} catch (error) {
  // если отказал хотя бы один запрос
  console.error(error);
}

Если нужно узнать, какой именно запрос упал, используйте Promise.allSettled.


Отмена асинхронных операций: AbortController

AbortController позволяет отменять асинхронные операции, чаще всего — fetch.

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
  .then((response) => response.json())
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.log('Request was aborted');
    } else {
      console.error('Request failed:', error);
    }
  });

// Отмена через 5 секунд
setTimeout(() => controller.abort(), 5000);

С async/await

async function loadUser(id, signal) {
  const response = await fetch(`/api/users/${id}`, { signal });
  return response.json();
}

const controller = new AbortController();
loadUser(1, controller.signal);

// При размонтировании компонента или смене запроса:
controller.abort();

React-пример

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then((response) => response.json())
    .then(setResults)
    .catch((error) => {
      if (error.name !== 'AbortError') {
        setError(error);
      }
    });

  return () => controller.abort();
}, [query]);

AbortController помогает избежать состояний-гонок (race conditions), когда старый запрос приходит позже нового.


Ключевые тезисы для интервью

  • Promise.all ожидает все промисы и отклоняется при первом отказе; Promise.race возвращает результат первого завершившегося промиса.
  • Promise.allSettled нужен, когда важно получить результат каждого промиса независимо от успеха или отказа.
  • async/await — синтаксический сахар над Promise; код после await выполняется как микрозадача.
  • Ошибки в async/await обрабатываются через try/catch, в цепочках Promise — через .catch().
  • Необработанный отказ промиса вызывает событие unhandledrejection; его можно отловить глобально.
  • AbortController позволяет отменить fetch и другие асинхронные операции, передавая signal в запрос.
  • Независимые запросы лучше выполнять параллельно через Promise.all, зависимые — последовательно через await.
  • Promise.then выполняется раньше setTimeout(..., 0), потому что микрозадачи имеют приоритет над макрозадачами в Event Loop.

Заключение

Promise и async/await — основа асинхронного программирования в JavaScript. Понимание состояний промиса, приоритета микрозадач над макрозадачами и методов комбинирования (all, race, allSettled, any) позволяет писать эффективный и предсказуемый код. Обработка ошибок через try/catch или .catch() и отмена операций через AbortController — обязательные навыки для стабильной работы с асинхронностью. Потренируйтесь переписывать вложенные callback’и на async/await и выбирать между последовательным и параллельным выполнением запросов.

Полезные ссылки