Перейти к основному содержимому

5.01. Асинхронность в JavaScript

Разработчику Архитектору

Асинхронность в JavaScript

JavaScript использует механизм асинхронности, обеспечивая одновременное выполнение нескольких задач «параллельно» без ожидания окончания предыдущих в очереди. Важно подчеркнуть, что JS однопоточный язык, а параллелизм имитируется благодаря event loop и асинхронным API.

Думаю, что этот абзац может быть очень непонятным для неподготовленного новичка. И это нормально. Давайте по полочкам.

JavaScript - однопоточный язык программирования. Это значит, что в каждый момент времени он может выполнять только одну инструкцию. Представьте, что у вас есть одна линия конвейера, по которой проходит одна деталь за раз. Никакого параллельного выполнения, как в многопоточных языках (например, Java или C#), в чистом JS нет. Инструкция 1, потом Инструкция 2. И пока Инструкция 1 не выполнится, поток блокируется и мы не переходим к Инструкции 2.

Но если JS выполняет только одну задачу за раз — как тогда он может загружать данные с сервера, реагировать на клики, устанавливать таймеры, читать файлы — и при этом не подвисать? Ответ - за счёт асинхронной модели выполнения, построенной на Event Loop и асинхронных API браузера или среды выполнения (например, Node.js).

Таким образом, здесь асинхронность не является многопоточностью.

Когда вы пишете:

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");

То можете ожидать из природы интерпретируемости JS, что выйдет результат 1, 2, 3, так как задержка в setTimeout указана 0 миллисекунд. Но вывод будет иным - 1, 3, 2. Почему? Потому что setTimeout — асинхронная операция, и она не блокирует выполнение кода. Вместо этого она передаёт свою функцию-коллбэк в внешнюю систему (браузер или Node.js), которая запускает таймер в фоне, а сам JS продолжает работать дальше. Именно Event Loop отвечает за то, когда и в каком порядке эти отложенные функции будут вызваны — после завершения текущей синхронной работы.

Чтобы понять асинхронность, нужно разобраться с архитектурой выполнения кода в JS. Она состоит из нескольких ключевых компонентов:

  1. Стек вызовов (Call Stack) - это стек, в котором хранятся функции, которые сейчас выполняются. JS работает по принципу LIFO (Last In, First Out) — последняя добавленная функция выполняется первой.
function greet() {
console.log("Привет!");
}
greet(); // → добавляется в стек, выполняется, удаляется

Когда greet() вызывается, она попадает в стек. После завершения — удаляется.

  1. Web APIs (или Host APIs) - это внешние среды, предоставляемые браузером или Node.js, такие как:
    • setTimeout, setInterval
    • fetch, XMLHttpRequest
    • addEventListener
    • requestAnimationFrame
    • Работа с DOM, файлами и т.д.

Когда вы вызываете setTimeout, JS не выполняет таймер сам. Он передаёт задачу в Web API, которое начинает отсчёт времени в фоне, независимо от JS. Поэтому и так получается - 1, 3, 2.

  1. Очереди: Task Queue (Macrotasks) и Microtask Queue. То есть, бывает два вида очередей - микрозадачи и макрозадачи. Когда асинхронная операция завершается, её коллбэк не выполняется сразу. Он помещается в одну из двух очередей:

Микрозадачи (очередь микрозадач) - высокий приоритет, и сюда попадают .then, .catch, .finally от промисов (Promise), queueMicrotask(), MutationObserver (отслеживание изменений DOM). Все микрозадачи выполняются до следующей макрозадачи.

Макрозадачи (очередь макрозадач) - низкий приоритет, и сюда попадают setTimeout, setInterval, setImmediate (в Node.js), события (click, input и т.д.), I/O операции. Одна задача из этой очереди выполняется за один цикл Event Loop.

  1. Event Loop (Цикл событий) - механизм, который постоянно проверяет:
    • пуст ли стек вызовов?
    • есть ли микрозадачи? Если да, то выполняет все из очереди микрозадач;
    • есть ли макрозадачи? Если да, то выполняет одну из очереди макрозадач;
    • возвращается к шагу (проверка стека вызовов).

Такой вот цикл - это работает по кругу снова и снова. Event Loop — это не поток и не таймер. Это просто цикл, который проверяет стек и очереди.

Пример.

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");

Код выглядит простым, и давайте глянем, как всё работает:

  1. console.log("1") — синхронный вызов → выполняется сразу → выводит 1;
  2. setTimeout(...) — передаёт коллбэк в Web API, которое начинает таймер (0 мс). Коллбэк ещё не в очереди, но будет помещён в очередь макрозадач, когда таймер сработает;
  3. Promise.resolve().then(...) — промис уже выполнен, .then помещает коллбэк в очередь микрозадач;
  4. console.log("4") — синхронный вызов → выполняется → выводит 4;
  5. Стек вызовов пуст → Event Loop начинает работать;
  6. Проверка очереди микрозадач → есть одна задача (console.log("3")) → выполняется → выводит 3;
  7. Очередь микрозадач пуста → Event Loop переходит к очереди макрозадач;
  8. Очередь макрозадач → есть setTimeout → выполняется → выводит 2.

Итого, в результате мы увидим 1, 4, 3, 2.

Важно: даже если setTimeout установлен на 0, он всегда ждёт, пока все микрозадачи будут выполнены. Промисы всегда приоритетнее таймеров.

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

Пример:

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

Что происходит:

  1. fetch — асинхронная операция → передаётся в Web API (браузер).
  2. JS не ждёт ответа → продолжает выполнять следующие строки.
  3. Когда сервер ответит, Web API помещает коллбэк из .then в очередь микрозадач.
  4. Как только стек освободится, Event Loop выполнит этот коллбэк.
  5. Таким образом, основной поток не блокируется, и страница остаётся отзывчивой.

А что такое асинхронная функция? Как можно понять, это будет как-то так:

async function load() {
console.log("A");
await fetch("/api"); // → ожидание
console.log("B");
}

Здесь создаётся асинхронная функция load(), которая логирует A, потом выполняет fetch по адресу /api, ожидает и логирует B.

Функция становится асинхронной не потому, что она использует await, а потому что:

  1. она помечена как async - всегда возвращает промис;
  2. внутри неё можно использовать await - логика приостанавливается на промисе;
  3. после await происходит продолжение функции - это микрозадача.
async function f() {
await 1; // await с примитивом → промис, разрешённый сразу
console.log("after await");
}
f();
console.log("sync");

Здесь создаётся асинхронная функция f(), которая ожидает 1 - это промис, который ожидает примитив, потом логирует «after await». А глобально происходит вызов f(), затем логирование sync. Вывод, как можно понять, будет сначала sync, потом after await. Потому что await это обёрнутый промис .then(), который является микрозадачей. А микрозадачи всегда выполняются после синхронного кода.

Итого, сначала синхронный код, потом все микрозадачи, потом одна макрозадача. Коллбэки сразу не вызываются, а попадают в очереди и ждут. А acync/await называют синтаксическим сахаром, потому что работа выполняется на том же механизме - промисы являются микрозадачами.

И если бы вам нужно было вывести и выполнить всё по порядку без какого-то ожидания…вы бы просто делали синхронный код. Да, у вас будет 1, 2, 3, к примеру, но если вывод «2» требует запроса к серверу, который может грузиться или считать какое-то время…то у вас программа остановится на этом месте и будет ждать, пока сервер соизволит ответить. И JavaScript всё ещё остаётся однопоточным, и делает вид, что выполняет несколько задач параллельно, благодаря Event Loop.

Механизмы асинхронности:

  1. Callback – функции, вызываемые после завершения операции.
  2. Promises (Промисы) – более удобная альтернатива колбэкам.
  3. Async/Await – синтаксический сахар над промисами.

Разберём их более детально.

Callback – фундаментальный паттерн, когда функция передаётся как аргумент и выполняется после завершения асинхронной операции. То есть, callback-функции – это функции, которые передаются в другую функцию и выполняются после какого-то события.

Представим, что мы просим друга сделать что-то и говорим ему: «Вот инструкция (функция). Выполни её, когда закончишь своё дело». Мы – JS-код, а друг – асинхронная функция:

сходи в магазинасинхронная операция
а когда вернёшься – позвони мнеcallback

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

Давайте разберём шаблон создания функции с коллбеком:

function имяФункции(параметры, callback) {
// какая-то асинхронная операция
// например: setTimeout, fetch, чтение файла и т.д.

// когда операция завершится:
if (успех) {
callback(null, результат); // первый аргумент — ошибка (null если успех)
} else {
callback(ошибка, null); // ошибка в первом аргументе
}
}

Стоит сразу отметить, что в Node.js и многих библиотеках используется ошибка первым аргументом (error-first callback). Это стандарт. Но давайте разбирать вышеприведенный шаблон.

Здесь, как мы видим, происходит обычное объявление обычной функции через function имяФункции(). К примеру, это может быть function fetchData(url, callback).

Далее мы видим, что в параметрах функции есть callback. Это один из параметров, в который передадут функцию. То есть, callback это не ключевое слово, а просто имя параметра, можно cb, onDone, handler, да что угодно.

И мы видим, что в тексте есть два вызова callback:

  • callback(null, data) - вызов коллбека с данными, к примеру при успехе будет callback(null, "данные");
  • callback(error, null) - вызов при ошибке, к примеру, callback("Ошибка сети", null).

Сложно? Технически, получается, вызывая эту функцию, мы просто в том месте, где callback, передаем функцию. К примеру, имяФункции(аргумент, имяФункцииКоллбека).

Давайте ещё пример:

function loadData(callback) {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
callback(null, "Данные загружены!");
} else {
callback("Ошибка загрузки", null);
}
}, 1000);
}

Здесь абсолютный аналог. callback - просто имя параметра, которое можете назвать как угодно, но мы будем подразумевать, что будем вызывать его. А в loadData(...) мы передаём функцию как аргумент — это и есть коллбэк.

Что следует запомнить:

  • Коллбэки не возвращают результат — они вызываются, когда операция завершится.
  • Коллбэки не имеют встроенной обработки ошибок — нужно вручную проверять первый аргумент.

Погнали дальше.

function loadData(callback) { // функция принимает callback
setTimeout(() => { //происходит эмуляция асинхронности
callback("Данные загружены!"); //через 1 секунду вызываем callback с результатом
}, 1000);
}

loadData((result) => { // передаём callback-функцию
console.log(result); // она сработает через 1 сек: "Данные загружены!"
});

В вышеприведённом примере это работает так, пошагово:

  1. loadData – это функция, которая ждёт callback (друг, ждущий инструкцию);
  2. Внутри loadData есть setTimeout – он имитирует долгую операцию (например, запрос к серверу).
  3. Через 1 секунду setTimeout вызывает callback("Данные загружены!").
  4. Мы передаём анонимную функцию (result) =>{ console.log(result); } как callback.
  5. Когда setTimeout срабатывает – callback выполняется, и в консоль выводится результат.

Callback применяется для загрузок данных с сервера, чтения файлов, таймеров (setTimeout, setInterval), обработки событий, или работы с API.

Пример (setTimeout):

setTimeout(() => {
console.log("Прошло 2 секунды!");
}, 2000);

() => { console.log() } – это callback.

Он выполнится после того, как пройдёт 2 секунды.

Пример (AJAX):

function fetchData(url, callback) {
fetch(url)
.then(response => response.json())
.then(data => callback(data)) // Передаём данные в callback
.catch(error => console.error(error));
}

fetchData("https://api.example.com/data", (data) => {
console.log("Получены данные:", data);
});

(data) => { console.log(data); } – callback, который сработает после загрузки данных.

Пример (обработчик клика):

button.addEventListener("click", () => {
console.log("Кнопка нажата!");
});

Вся стрелочная функция – это callback.

Она выполнится после клика на кнопку.

Таким образом, callback – это функция, которая выполнится после какого-то события.

Promises (промисы) – объекты-обёртки для асинхронных операций, которые:

  • ждут завершения (pending);
  • по завершении получают fulfilled или rejected (возвращают результат или ошибку);
  • позволяют цеплять обработчики (then/catch).
Pending → Fullfulled с результатом или Rejected с ошибкой.

Promise (от английского) – «обещание» JavaScript сделать что-то асинхронное и сообщить результат: успех (fullfulled), ошибка (rejected), ожидание (pending) – ещё выполняется.

image-6.png

Итого, промисом является некий объект, представляющий будущий результат асинхронной операции, который может быть в одном из трёх состояний - ожидание, успех или ошибка. Давайте подготовим шаблон для создания промиса и разберём его:

const имяПеременной = new Promise((resolve, reject) => {
// асинхронная операция (таймер, запрос, чтение файла...)

if (операцияУспешна) {
resolve(результат); // промис переходит в fulfilled
} else {
reject(ошибка); // промис переходит в rejected
}
});

Здесь мы создаём переменную как новый объект Promise. А resolve и reject — это функции, которые вы не создаёте, а получаете как параметры от new Promise.

new Promise(...) означает создание объекта-обещания.

(resolve, reject) означают функции, которые будут завершать это обещание.

resolve(данные) означает «всё хорошо, вот результат», к примеру, resolve(user).

reject(ошибка) означает «всё плохо, вот причина», к примеру, reject("Сеть недоступна").

Пример:

const getUser = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve({ id: 1, name: "Алекс" });
} else {
reject(new Error("Не удалось загрузить пользователя"));
}
}, 1000);
});

Здесь создаётся переменная getUser, выполняется операция, и при успехе передаётся результат в параметрах resolve, иначе - reject и ошибка как новый объект с текстом.

Использование промиса выполняется тремя методами - .then, .catch, .finally. Шаблонно можно сделать так:

имяПеременной
.then((результат) => {
// выполняется, если промис fulfilled
// результат — то, что передали в resolve(...)
return новыйРезультат; // можно возвращать промис или значение
})
.catch((ошибка) => {
// выполняется, если промис rejected
// ошибка — то, что передали в reject(...)
console.error("Ошибка:", ошибка);
})
.finally(() => {
// выполняется ВСЕГДА, независимо от результата
// полезно для очистки, логирования, скрытия лоадера
console.log("Загрузка завершена");
});
  • .then(result => { ... }) здесь вызывается при успехе (resolve), может вернуть новый промис или значение. Не обязателен, но без него смысла в промисе нет.
  • .catch(error => { ... }) вызывается при ошибке (reject), ловит ошибки из resolve, reject или предыдущих .then, и рекомендуется, чтобы ошибки не потерялись. Без него ошибки «проглатываются», и вы не узнаете, что что-то пошло не так. Можно писать только .then, но если произойдёт ошибка, она не будет обработана, и в консоли появится unhandled promise rejection.
  • .finally(() => { ... }) выполняется всегда, после .then или .catch, не получает аргументов, и является опциональным.

.then и .catch возвращают новый промис, поэтому можно строить цепочки. Каждый .then может возвращать промис — тогда следующий .then дождётся его.

Пример:

getUser
.then((user) => {
console.log("Пользователь:", user);
return user.id;
})
.then((id) => {
console.log("ID:", id);
})
.catch((error) => {
console.error("Ошибка:", error.message);
})
.finally(() => {
console.log("Готово!");
});

Здесь мы указывает что если user то печатаем одно, если id, то другое. А в любом случае выводим Готово!.

Как мы ранее усвоили, .then и .catch — это микрозадачи, которые выполняются после синхронного кода, но до setTimeout.

Чем промис отличается от колбэка?

Колбэк (Callback)Промис (Promise)
Функция, которая вызывается после операции.Объект, который представляет будущий результат и сам управляет своим состоянием.
Мы даём номер телефона (колбэк) курьеру и говорим: «Позвони, как довезёшь». И если курьер не позвонит – пицца зависнет.Нам дают чек на заказ (промис), у которого есть статусы. И мы точно знаем, что пицца не потерялась, чек (промис) – гарантия доставки. Мы можем заниматься своими делами. Пицца доставлена – можем кушать. Сгорела (rejected) – мне вернут деньги.

Промисы предоставляют удобные статические методы для работы с одним или несколькими промисами. Часто их могут называть Promise API:

  • Promise.resolve(value) - cоздаёт уже выполненный (fulfilled) промис с переданным значением. Используется для оборачивания значения в промис, когда нужно вернуть промис, но результат уже известен, и чтобы унифицировать интерфейс: всегда возвращать промис, даже если данные синхронные. Если передать в Promise.resolve() другой промис — он просто вернёт его без изменения (но гарантирует, что это будет промис).
  • Promise.reject(reason) - создаёт уже отклонённый (rejected) промис с указанной ошибкой. Можно использовать для создания ошибок в цепочках, в условных ветвлениях, когда нужно сразу «упасть».
  • Promise.all(iterable) - ждёт все промисы, завершается только тогда, когда все успешно выполнились. Возвращает массив результатов. Если хотя бы один промис отклонился, весь Promise.all переходит в состояние rejected. Это «жёсткий» режим. Использовать, когда все данные нужны обязательно (например, профиль + настройки + подписки).
  • Promise.allSettled(iterable) ждёт завершения всех промисов, независимо от успеха или ошибки. Возвращает массив объектов с результатами. Отличие от all в том, что allSettled никогда не отклоняется, всегда ждёт всех. Полезно, когда важны все ответы, даже ошибочные. Пример - массовая отправка уведомлений, если нужно узнать, какие дошли, а какие нет.
  • Promise.any(iterable) - возвращает результат первого успешного промиса. Остальные игнорируются. Падает только если все промисы отклонены (AggregateError — ошибка, содержащая массив всех причин). Использовать для реализации резервирования (failover): попробовать несколько источников, взять первый рабочий.
  • Promise.race(iterable) - возвращает результат первого завершившегося промиса, независимо от успеха или ошибки. Очень чувствителен к скорости: если первый промис — ошибка, вся цепочка падает. Опасен: может «поймать» ошибку раньше, чем успеют выполниться успешные промисы. Часто используется с таймаутами для защиты от зависаний.

Давайте пробежимся ещё раз по промису.

Как создать промис?

const myPromise = new Promise((resolve, reject) => {
// Симуляция асинхронной операции (например, запрос к серверу, чтение файла, таймер)
const isSuccess = /* Логика проверки успеха или ошибки */;
if (isSuccess) {
resolve(/* Результат успешной операции */);
} else {
reject(/* Причина ошибки */);
}
});

Как использовать промис?

myPromise
.then((result) => {
// Обработка успешного результата
console.log("Успех:", result);
return /* Новый результат для следующего шага */;
})
.catch((error) => {
// Обработка ошибки
console.error("Ошибка:", error);
})
.finally(() => {
// Выполняется всегда, независимо от результата
console.log("Операция завершена.");
});

Разбирая пример с пиццей, можно сделать это кодом в два этапа:

  1. Создание промиса:
const pizzaPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.5; // 50% шанс успеха
if (isSuccess) {
resolve("Пицца доставлена!"); // Успех
} else {
reject("Пицца сгорела в печи!"); // Ошибка
}
}, 2000); // Через 2 секунды
});
  1. Использование промиса:
pizzaPromise
.then((result) => {
console.log(result); // "Пицца доставлена!"
})
.catch((error) => {
console.error(error); // "Пицца сгорела в печи!"
});
  1. new Promise создаёт объект с двумя функциями:
    • resolve() – вызывается при успехе;
    • reject() – вызывается при ошибке.
  2. Через 2 секунды «кухня» (setTimeout) сообщает результат.
  3. .then() ловит успех, .catch() – ошибку.

Хороший пример использования промисов - fetch API, современный встроенный в браузер (и Node.js с определённых версий) способ делать HTTP-запросы к серверам. Он возвращает промис. Сама по себе функция fetch() используется для того, чтобы получить данные с сайта, отправить форму, загрузить форму или взаимодействовать с API. Это замена старому способу — XMLHttpRequest.

Базовый синтаксис:

fetch(url, options)
.then(response => {
// Обработка ответа
})
.catch(error => {
// Обработка ошибок сети
});

url — адрес, куда отправляется запрос.

options — необязательный объект: метод (GET, POST и т.д.), заголовки, тело запроса и др.

fetch(...) — отправляет запрос по указанному адресу. Сервер отвечает (например, списком пользователей).

then(response => - превращает ответ в понятные данные (обычно JSON, к примеру, .then(response => response.json())).

Смысл в том, что сразу данные мы не получаем - нужно дождаться, пока промис выполнится. Когда fetch получает ответ от сервера, он передаёт его в .then() как объект response. У этого объекта есть полезные свойства и методы:

  • response.ok - true, если статус 200–299 (успех), иначе false;
  • response.status - код ответа;
  • response.statusText - текст статуса;
  • response.headers - заголовки ответа;
  • response.url - URL, по которому был получен ответ.
  • response.json() - парсит ответ как JSON;
  • response.text() - читает ответ как обычный текст;
  • response.blob() - для файлов (картинок, PDF и тд);
  • response.formData() - для форм.

fetch НЕ выбрасывает ошибку при HTTP-ошибках (404, 500 и т.п.). Он считает, что запрос успешно дошёл до сервера, а значит — технически всё ок. Поэтому всегда проверяйте response.ok, если важен успех операции:

fetch('/api/user')
.then(response => {
if (!response.ok) {
throw new Error(`Ошибка ${response.status}`);
}
return response.json();
})
.then(user => console.log(user));

По умолчанию fetch делает GET-запрос (получает данные). Чтобы отправить данные — нужно указать параметры:

fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Мой пост',
content: 'Привет, мир!'
})
})
.then(response => response.json())
.then(newPost => {
console.log('Создано:', newPost);
});

fetch() используется повсюду - это получение данных в React/Vue/Angular, отправка форм без перезагрузки страницы, работа с внешними API (погода, карты, платежи), чаты, уведомления, автоподгрузка контента. В старых браузерах вроде IE он не поддерживается, по умолчанию куки не отправляет, не имеет встроенной защиты от таймаутов и прогресс загрузки нельзя легко отследить.

Паттерны использования промисов:

  1. Цепочка (chaining) – каждый .then возвращает новый промис:
fetch("/api/user") // Запрашиваем пользователя
.then((response) => response.json()) // Преобразуем в JSON
.then((user) => fetch(`/api/posts?userId=${user.id}`)) // Запрашиваем посты
.then((response) => response.json()) // Ещё раз JSON
.then((posts) => console.log("Посты пользователя:", posts))
.catch((err) => console.error("Ошибка:", err)); // Ловим все ошибки в цепочке
  1. Параллельное выполнение – запускаются несколько промисов одновременно и ждём все результаты. Если все промисы успешны - .then() получит массив результатов. Если хотя бы один упадёт – сработает .catch.
Promise.all([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/comments")
])
.then(([users, posts, comments]) => {
console.log("Все данные загружены:", { users, posts, comments });
})
.catch((err) => {
console.error("Один из запросов провалился:", err);
});

  1. Ранний выход (Promise.resolve/reject):
let cachedData = null;

function getData() {
if (cachedData) {
return Promise.resolve(cachedData); // Немедленный успех
}
return fetch("/api/data")
.then((data) => {
cachedData = data; // Кешируем
return data;
});
}

getData().then((data) => console.log(data));

Промисы используют микрозадачи (microtask queue) – они выполняются сразу после текущего синхронного кода, но перед макрозадачами (setTimeout, UI-рендеринг).

console.log("Старт");

setTimeout(() => console.log("setTimeout"), 0);

Promise.resolve()
.then(() => console.log("Промис 1"))
.then(() => console.log("Промис 2"));

console.log("Конец");

Промис – контейнер для будущего результата.

Promise.all запускает операции параллельно.

Любая функция с async автоматически возвращает промис. Внутри неё можно использовать await.

async function makeCoffee() {
return "Эспрессо"; // Автоматически оборачивается в промис!
}

makeCoffee().then(console.log); // Выведет "Эспрессо"

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

async function makeBreakfast() {
const coffee = await makeCoffee(); // Ждём кофе
const sandwich = await makeSandwich(); // Затем бутерброд
return `${coffee} + ${sandwich}`;
}

makeBreakfast().then(console.log); // "Эспрессо + Брускетта"

Async/Await – «синтаксический сахар», синхронный стиль, который позволяет писать код, будто он синхронный. Это комбинация async и await:

  • async функция всегда возвращает промис;
  • await приостанавливает выполнение функции, пока промис не разрешится.
async function loadUserAndPosts() {
try {
const user = await fetch('/api/user'); // Ждём пользователя
const posts = await fetch(`/api/posts?userId=${user.id}`); // Ждём посты
return { user, posts }; // Возвращаем результат
} catch (err) {
console.error('Ошибка загрузки:', err);
throw err; // "Пробрасываем" ошибку дальше
}
}

Важные ньюансы:

  • await работает только в async-функциях;
  • async-функция всегда возвращает промис;
  • async говорит, что функция работает с промисами;
  • await – «стоп-сигнал» для кода, который говорит «Жди здесь, пока промис не выполнится».

AJAX (Asynchronous JavaScript and XML) – технология для загрузки данных без перезагрузки страницы. Браузер отправляет HTTP-запрос в фоне, сервер возвращает данные (в JSON/XML), JS динамически обновляет страницу.

Классический AJAX-алгоритм:

  • создать запрос (Fetch API или XMLHttpRequest);
  • отправить запрос с параметрами (метод, заголовки, тело);
  • обработать ответ (JSON, текст, бинарные данные);
  • обновить UI или обработать ошибку.
async function fetchData(url, method = 'GET', body = null) {
const options = { method };
if (body) options.body = JSON.stringify(body);

const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);

return response.json(); // Автоматический парсинг JSON
}

Event Loop – цикл событий, состоящий из фаз:

  • выполнить синхронный код до конца;
  • обработать микрозадачи (промисы, queueMicrotask);
  • обработать макрозадачи (setTimeout, setInterval, UI-рендеринг).
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');