Промисы и 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.