Promises, async/await, and the event loop.
Agent execution model
В спецификации ECMAScript Agent — это самостоятельный исполнитель JavaScript-кода.
Каждый агент работает в одном потоке и имеет свой собственный стек вызовов (Execution Context Stack) и очередь задач (Job Queue).
Он может быть:
-
Веб-страницей в браузере
-
Web Worker-ом
-
Node.js процессом
Каждый агент имеет свой собственный Realm, который состоит из такой информации, как, например,
глобальный объект список прототипов и глобальные переменные
Кроме того, каждый агент управляет своими собственными:
Heap (of objects)
Queue (of jobs)
Stack (of execution contexts)
в HTML (и других языках) это и называется event loop, который позволяет асинхронно программировать в однопоточном
JavaScript

-
Microtask Queue — очередь микрозадач:
-
Promise.then/catch/finally
-
Функции, добавленные через
queueMicrotask() -
MutationObserver
-
-
Task Queue— крупные асинхронные задачи, как:
-
-
setTimeout, setInterval,
-
События DOM и их обработчики
-
Jobs

-
JavaScript — однопоточный
-
JavaScript выполняет код, управляя его контекстом выполнения и стеком вызовов
-
Асинхронность достигается через Event Loop (события) и очереди задач
-
Нет sleep, встроенной функции, которая просто "приостанавливает" выполнение кода на какое-то время
Найдите неверное утверждение:
Все верные!

PROMISE
- Объект, представляющий результат асинхронной операции.
-
Объект, реализующий Thenable интерфейс.
-
Имеет 3 состояния: pending, fulfilled, rejected.
-
Иммутабелен после перехода в fulfilled или rejected.


THENABLE
Это любой объект с методом then(), но не обязательно поддерживающий внутреннее состояние или спецификацию, как у промиса.
Может не поддерживать правильную логику для обработки состояний или асинхронных операций.
const notARealPromise = {
then: function(resolve, reject) {
// Не вызывает resolve или reject
console.log("Then called, but no resolve/reject");
}
};
notARealPromise.then(console.log); // Output: "Then called, but no resolve/reject"Внутренние свойства промиса
-
[[PromiseState]]: "pending", "fulfilled", "rejected" -
[[PromiseResult]]: значение или ошибка -
[[PromiseFulfillReactions]]очередь реакций на успешное выполнение /[[PromiseRejectReactions]]очередь реакций на отклонение -
[[PromiseIsHandled]]для трекинга unhandled rejection
Reactions
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Success!"), 1000); // Промис будет разрешён через 1 секунду
});
// Первая реакция
promise.then(result => {
console.log("First reaction:", result); // добавляется в `[[PromiseFulfillReactions]]`
});
// Вторая реакция
promise.then(result => {
console.log("Second reaction:", result); // добавляется в `[[PromiseFulfillReactions]]`
});
Когда промис становится разрешённым (fulfilled) или отклонённым (rejected), его колбэки попадают в очередь реакций ([[PromiseFulfillReactions]] или [[PromiseRejectReactions]]).
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Failure!"); // Промис будет отклонён (rejected) через 1 секунду
}, 1000);
});
// Первая реакция на отклонение (попадает в `[[PromiseRejectReactions]]`)
promise
.catch(error => {
console.log("First rejected reaction:", error);
});
// Через второй аргумент в `.then()` вторая реакция (попадает в `[[PromiseRejectReactions]]`)
promise
.then(
result => {
console.log("This will never log, as the promise is rejected.");
},
error => {
console.log("Second rejected reaction:", error); // попадает в `[[PromiseRejectReactions]]`
}
);
// Третья реакция на отклонение (попадает в `[[PromiseRejectReactions]]`)
promise
.catch(error => {
console.log("Third rejected reaction:", error);
});
-
Эти колбэки не выполняются сразу. Они ставятся в очередь микротасков (microtask queue), а не в очередь обычных событий (таких как
setTimeout). -
Обработка микротасков происходит после завершения текущего синхронного кода, но до выполнения любых других асинхронных операций, таких как
setTimeout,setIntervalили другие..
Promise Resolution Procedure
Рекурсивная распаковка thenable-объектов
let promise1 = new Promise((resolve) => {
resolve(new Promise((resolve) => {
resolve("Пример");
}));
});
promise1.then(result => {
console.log(result); // "Пример"
});Когда промис вызывает .then(), и результатом является другой промис:
- Мы ждем разрешения этого второго промиса (процесс рекурсивный).
- Если результатом является промис, то мы снова ждем его завершения, и так до тех пор, пока не получим финальный результат.
Циклические ссылки → TypeError
let promise1 = new Promise((resolve) => {
resolve(promise1); // Промис ссылается сам на себя
});
promise1.then(result => {
console.log(result);
}).catch(error => {
console.log(error); // TypeError: Chaining cycle detected for promise
});-
Здесь промис
promise1ссылается на сам себя, что создает циклическую зависимость. -
При попытке разрешить такой промис, возникает ошибка
TypeError: Chaining cycle detected for promise. -
Это предотвращает зацикливание в цепочке промисов.
then/catch/finally
-
Все методы возвращают новый Promise
-
then(onFulfilled, onRejected) -
catch(fn)=then(null, fn) -
finally(fn)вызывается всегда, но не меняет результат
then
Метод then используется для обработки успешного завершения промиса. Он принимает два аргумента:
-
Первый аргумент (обязателен): функция, которая будет вызвана, когда промис разрешится (т.е. когда его статус станет
fulfilled). -
Второй аргумент (необязателен): функция, которая будет вызвана, если промис будет отклонен (т.е. когда его статус станет
rejected).
catch
Метод catch используется для обработки ошибок, возникших в процессе работы с промисами. Он перехватываетисключения, выброшенные как в самом промисе, так и в любой функции, использующей then.
Важно, что catch будет ловить любые ошибки, если они не были обработаны внутри промиса через второй аргумент then.
new Promise((_, reject) => reject('Ошибка'))
.catch(error => {
console.log(error); // 'Ошибка'
});finally
Метод finally выполняет код, который должен быть выполнен в любом случае, независимо от того, был ли промис разрешен или отклонен. Это полезно для выполнения действий, которые должны происходить всегда (например, очистка ресурсов, закрытие соединений и т. д.).
async/await
async/await — это синтаксический сахар над промисами. С ними асинхронный код выглядит как синхронный, что делает его понятнее и читаемее, официально добавлены в ECMAScript 2017 (ES8).
Promise.resolve(10).then(value => {
console.log(value); // 10
});
async function bar() {
let value = await Promise.resolve(10);
console.log(value); // 10
}async-функция возвращает Promise
Любая функция, помеченная как async, всегда возвращает промис. Даже если в ней не используется await, она все равно будет возвращать промис
async function myFunction() {
return 42; // Это автоматически будет wrapped в Promise
}
myFunction().then(console.log); // Выведет 42await — ждет результат (или ошибку)
Ключевое слово await используется внутри async-функции и ожидает разрешения (или отклонения) промиса. Он "приостанавливает" выполнение текущей async-функции до получения результата.
Под капотом — то же, что и then, но с паузой в стейте выполнения
Внутри async/await все работает через промисы, просто синтаксис делает его более читаемым и последовательным. Использование await заменяет цепочку .then(), делая код более похожим на синхронный, но асинхронный.
async function example() {
const result = await new Promise(resolve => setTimeout(() => resolve('Привет!'), 1000));
console.log(result); // Выведет 'Привет!' через 1 секунду
}
example();
Как работает async/await (внутренне)
-
JS превращает функцию в стейт-машину
Когда компилятор видитasync-функцию, он фактически превращает её в стейт-машину, которая выполняется поэтапно. Каждый раз, когда код встречаетawait, выполнение функции "приостанавливается", и управление передается обратно в цикл событий.
-
На каждой итерации
await:
-
Выполнение останавливается на моменте
awaitдо тех пор, пока промис не будет разрешен или отклонен. -
Возвращается
Promise, который будет разрешен значением или ошибкой. - После resolve — продолжение исполнения функции, начиная с того места, где выполнение было приостановлено.
Awaitable values
Что может быть передано в await:
-
Promise — будет ждать разрешения или отклонения.
-
Любой thenable — объект, у которого есть метод
.then(). Это может быть не только объектPromise, но и любой объект, реализующий интерфейсthenable. -
Обычное значение — сразу возвращает значение без ожидания.
//Внутри await, движок делает что-то вроде:
Promise.resolve(значение)
await 42
/// ====>
await Promise.resolve(42)-
Если значение — обычное (например, 42), оно сразу возвращается, но await всё равно делает паузу до следующей микротаски.
Методы Promise

Поведение в цикле
Последовательные запросы
Как работает: Каждый запрос выполняется по очереди, следующий начинается только после завершения предыдущего.
-
Минусы:
Медленно. Все запросы выполняются один за другим.
Неэффективно. Блокировка на каждом запросе.
for (const url of urls) {
await fetch(url); // последовательно!
}Параллельные запросы
-
Как работает: Все запросы отправляются одновременно,
Promise.allждёт их завершения
await Promise.all(urls.map(fetch));
// параллельно-
Преимущества:
-
Быстрее. Все запросы выполняются одновременно.
-
Эффективно. Максимальное использование ресурсов сети и сервера.
-
-
Минусы:
-
Могут возникнуть проблемы с ресурсами при слишком большом количестве параллельных запросов.
-
Ошибки в одном запросе прерывают всю операцию (нужна обработка ошибок).
-
Promise нельзя отменить
Когда создается промис, его выполнение начинается немедленно, и отменить его невозможно. Промис представляет собой результат асинхронной операции, которая либо завершится успешно, либо с ошибкой. У него нет встроенной логики для остановки или отмены операции.
Использование AbortController
-
Это объект, который позволяет отменить асинхронную операцию, такую как запросы с
fetch. -
Он может использоваться для отмены асинхронных операций, если они не завершились вовремя или если они больше не нужны.
const controller = new AbortController();
const signal = controller.signal;
fetch(url, { signal }) // Передаем signal в fetch для отслеживания отмены
.then(response => {
// обработка ответа
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Запрос был отменен');
}
});
// Отмена запроса
controller.abort();
Сancellable wrapper
Обертка для промиса, которая будет отслеживать отмену через флаг или сигнал.
class CancellablePromise {
constructor(executor) {
this.cancelled = false;
this.promise = new Promise((resolve, reject) => {
executor(resolve, reject);
});
}
cancel() {
this.cancelled = true;
}
then(onFulfilled, onRejected) {
return this.promise.then(
value => (this.cancelled ? Promise.reject('Cancelled') : onFulfilled(value)),
onRejected
);
}
}
// Пример использования:
const cancelable = new CancellablePromise((resolve, reject) => {
setTimeout(() => resolve('Done'), 2000);
});
cancelable.then(result => console.log(result));
cancelable.cancel(); // Останавливает выполнение
Unhandled Rejections
Спецификация JavaScript не предоставляет стандартного способа обработки необработанных отклонений промисов. То, как эти отклонения обрабатываются, зависит от среды выполнения (например, Node.js или браузер).
Node.js
-
Ранее: В более ранних версиях Node.js необработанное отклонение промиса приводило к крашу процесса.
-
Сейчас: Node.js генерирует предупреждение, если промис отклоняется, но его ошибка не обрабатывается (например, через
.catch()илиtry-catch).
process.on('unhandledRejection', (reason, promise) => {
console.log('Необработанное отклонение промиса:', reason);
// Здесь можно предпринять действия, например, логировать ошибку
});
Браузеры
-
Chrome, Firefox, Safari, Edge поддерживают
window.onunhandledrejectionдля глобальной обработки необработанных отклонений.
window.onunhandledrejection = (event) => {
console.log('Необработанное отклонение промиса:', event.reason);
// Здесь можно выполнить действия, например, логировать ошибку
};Promises, async-await, and the event loop.
By Ksenia
Promises, async-await, and the event loop.
- 46