Промисы и async/await (JS)

Материал из llmdocs — справочник разработчика
Промисы и async/await (JS)
ЯзыкJavaScript (ECMAScript)
Promise в стандартеES2015 (ES6)
async/await в стандартеES2017
Предшественникиколбэки, Promises/A+, jQuery Deferred
Состояния промисаpending, fulfilled, rejected
КомбинаторыPromise.all, allSettled, race, any

Промис (англ. Promise — обещание) — объект JavaScript, представляющий результат асинхронной операции, который будет известен позже: ответ сервера, содержимое файла, срабатывание таймера. async/await — синтаксис, позволяющий писать код с промисами так, будто он выполняется последовательно. Вместе они — основной способ работы с асинхронностью в браузере и в Node.js.[2]

Зачем это нужно

JavaScript выполняется в одном потоке с циклом событий: долгие операции нельзя ждать синхронно, иначе страница или сервер «замёрзнут». Исторически результат передавали в колбэк — функцию, вызываемую по готовности. При нескольких зависимых шагах колбэки вкладывались друг в друга («ад колбэков», callback hell), а обработку ошибок приходилось повторять на каждом уровне.

Идея «обещаний» (futures) известна с 1970-х; в JavaScript её популяризировали библиотеки (Q, Bluebird, jQuery Deferred), а совместимость между ними обеспечила спецификация Promises/A+ (2012).[4] Встроенный Promise вошёл в стандарт ES2015, синтаксис async/await — в ES2017.[1]

Промис

У промиса три состояния: ожидание (pending), выполнен (fulfilled) со значением, отклонён (rejected) с причиной. Из ожидания он переходит ровно один раз и дальше не меняется.

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

fetch("/api/articles")
  .then((res) => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();             // возврат промиса — цепочка ждёт его
  })
  .then((data) => console.log(data.length))
  .catch((err) => console.error("ошибка:", err))   // ловит ошибки всей цепочки
  .finally(() => console.log("готово"));

Метод then всегда возвращает новый промис, поэтому вызовы выстраиваются в плоскую цепочку, а один catch в конце обрабатывает ошибку на любом шаге — аналог try/catch для асинхронного кода.

async/await

Функция, объявленная с async, всегда возвращает промис. Внутри неё await приостанавливает выполнение функции (но не потока!) до разрешения промиса и возвращает его значение или выбрасывает исключение.[3]

async function loadArticle(slug) {
  try {
    const res = await fetch(`/wiki/${slug}.md`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.text();
  } catch (err) {
    console.error(err);
    return null;
  }
}

Код выглядит синхронным, но пока функция ждёт ответа, цикл событий обрабатывает другие задачи. В ES-модулях разрешён await на верхнем уровне. Сходная модель есть в Python — asyncio — и в Rust (async в Rust и Tokio).

Параллельное ожидание

Частая ошибка — последовательные await для независимых операций: каждый запрос ждёт предыдущий. Комбинаторы запускают их одновременно:

Метод Результат
Promise.all(list) массив значений; отклоняется при первой ошибке
Promise.allSettled(list) статус каждого промиса, никогда не отклоняется
Promise.race(list) первый завершившийся (успехом или ошибкой)
Promise.any(list) первый успешный; ошибка, только если упали все
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);

// таймаут для запроса
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

Отменить сам промис нельзя; для отмены операций используется AbortController и его сигнал, который понимают fetch и многие API Node.js. Метод Promise.withResolvers() (ES2024) упрощает создание промиса, разрешаемого извне.[6]

Микрозадачи

Колбэки промисов выполняются как микрозадачи: после текущего синхронного кода, но раньше обычных задач (таймеров, событий ввода-вывода).[5]

setTimeout(() => console.log("3: задача"), 0);
Promise.resolve().then(() => console.log("2: микрозадача"));
console.log("1: синхронно");

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

Типичные ошибки

  • Забытый await — функция получает промис вместо значения; TypeScript и линтеры помогают это поймать.
  • Необработанное отклонение — в Node.js по умолчанию завершает процесс.
  • await внутри forEach не ждёт — нужен for...of или Promise.all с map.

См. также

Источники

  1. ECMAScript Language Specification — Promise Objects
  2. MDN — Using promises
  3. MDN — async function
  4. Promises/A+ specification
  5. HTML Standard — Event loops (tasks and microtasks)
  6. MDN — Promise.withResolvers