$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКак устроен async/await в JavaScript: Event Loop, микрозадачи и очерёдность выполнения

Как устроен async/await в JavaScript: Event Loop, микрозадачи и очерёдность выполнения

$ sudo teach IT
·18 июля 2026 г.·8 мин·60
Как устроен async/await в JavaScript: Event Loop, микрозадачи и очерёдность выполнения

Погружаемся в механику асинхронного JS: почему промисы выполняются раньше setTimeout, как Event Loop обрабатывает очереди и какие ошибки совершают новички при работе с async/await.

Содержание
Однопоточность и проблема блокировкиАрхитектура: Call Stack, Web APIs и очереди задачCall StackWeb APIsОчереди задач: Macrotask Queue и Microtask QueueEvent Loop: как это всё связаноБесконечная очередь микрозадач — реальная опасностьasync/await: синтаксический сахар над промисамиЧто происходит под капотомТипичные ошибки при работе с async/await1. Забытый await2. Последовательное ожидание вместо параллельного3. Неправильная обработка ошибок4. async в forEach5. Необработанные отклонения промисовПорядок выполнения: практический тестНесколько слов о Promise.allSettled, Promise.race и Promise.anyВывод

Асинхронность — одна из тех тем, которую большинство JavaScript-разработчиков считают понятой раньше, чем она действительно понята. Синтаксис async/await выглядит обманчиво просто: пишешь await, и код «ждёт». Но стоит столкнуться с неожиданным порядком вывода в консоли или с промисом, который ведёт себя не так, как ожидалось, — и становится ясно, что поверхностного знания недостаточно.

В этой статье разберём, как на самом деле работает асинхронный JavaScript: что такое Event Loop, чем микрозадачи отличаются от макрозадач, почему Promise.resolve() выполняется раньше setTimeout(..., 0) и какие ошибки чаще всего допускают при работе с async/await.


Однопоточность и проблема блокировки

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

Именно для решения этой проблемы и существует асинхронная модель выполнения. Но «асинхронность» в JS — это не параллельность и не многопоточность. Это механизм откладывания задач и их последовательного выполнения в нужный момент.

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


Архитектура: Call Stack, Web APIs и очереди задач

Call Stack

Call Stack (стек вызовов) — это структура данных, в которой хранятся текущие вызовы функций. Когда вы вызываете функцию, она помещается на вершину стека. Когда функция завершается — снимается с него.

code
function greet(name) {
  return `Hello, ${name}`;
}

function main() {
  const message = greet('Alice');
  console.log(message);
}

main();
// Call Stack: main → greet → (возврат) → console.log → (возврат) → main завершается

Пока в стеке что-то есть, JavaScript занят. Никакие другие задачи не выполняются.

Web APIs

Когда вы вызываете setTimeout, fetch или addEventListener, браузер (или Node.js) берёт эту задачу на себя и выполняет её вне стека — в так называемых Web APIs (или C++ APIs в Node.js). JavaScript-движок при этом свободен и продолжает выполнять следующий код.

Когда операция завершается (таймер истёк, данные пришли), результат помещается в одну из очередей задач.

Очереди задач: Macrotask Queue и Microtask Queue

Здесь начинается самое важное. Существуют две очереди:

  • Macrotask Queue (очередь макрозадач) — сюда попадают колбэки setTimeout, setInterval, setImmediate (Node.js), события ввода/вывода.
  • Microtask Queue (очередь микрозадач) — сюда попадают обработчики промисов (.then, .catch, .finally) и колбэки queueMicrotask().

Event Loop: как это всё связано

Event Loop — это бесконечный цикл, который следит за состоянием Call Stack и очередей. Его алгоритм прост, но критически важен:

  1. Если Call Stack пуст — взять одну задачу из Macrotask Queue и выполнить её.
  2. После выполнения каждой макрозадачи — полностью опустошить Microtask Queue.
  3. Повторить.

Именно пункт 2 объясняет, почему промисы выполняются раньше setTimeout.

code
console.log('1: синхронный код');

setTimeout(() => {
  console.log('2: setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise.then');
});

console.log('4: ещё синхронный код');

// Вывод:
// 1: синхронный код
// 4: ещё синхронный код
// 3: Promise.then
// 2: setTimeout

Разберём по шагам:

  1. console.log('1') — выполняется синхронно, попадает в стек и сразу выполняется.
  2. setTimeout — передаётся в Web API, колбэк будет помещён в Macrotask Queue через 0 мс.
  3. Promise.resolve().then(...) — промис уже разрешён, колбэк немедленно попадает в Microtask Queue.
  4. console.log('4') — выполняется синхронно.
  5. Call Stack пуст. Event Loop проверяет Microtask Queue — там есть задача. Выполняет её: console.log('3').
  6. Microtask Queue пуста. Event Loop берёт задачу из Macrotask Queue: console.log('2').

Ключевой момент: микрозадачи всегда выполняются до следующей макрозадачи, независимо от того, когда они были добавлены.

Бесконечная очередь микрозадач — реальная опасность

Раз микрозадачи выполняются до следующей макрозадачи, рекурсивное добавление микрозадач заблокирует Event Loop:

code
function infiniteMicrotasks() {
  Promise.resolve().then(infiniteMicrotasks);
}

infiniteMicrotasks();
// setTimeout здесь никогда не выполнится
setTimeout(() => console.log('Это никогда не выведется'), 100);

На практике это встречается редко, но понимать механизм важно.


async/await: синтаксический сахар над промисами

async/await — это не новая модель асинхронности. Это синтаксический сахар над промисами, который делает асинхронный код визуально похожим на синхронный.

code
// С промисами
function fetchUser(id) {
  return fetch(`/api/users/${id}`)
    .then(response => response.json())
    .then(data => data.user);
}

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

Оба варианта делают одно и то же. async-функция всегда возвращает промис. await приостанавливает выполнение внутри функции (не блокируя поток), ожидая разрешения промиса.

Что происходит под капотом

Когда интерпретатор встречает await, он:

  1. Вычисляет выражение справа от await.
  2. Если это промис — подписывается на его разрешение через .then().
  3. Приостанавливает выполнение текущей async-функции и возвращает управление в Call Stack.
  4. Когда промис разрешается, продолжение функции помещается в Microtask Queue.
code
async function example() {
  console.log('A');
  await Promise.resolve();
  console.log('B'); // это микрозадача
}

console.log('1');
example();
console.log('2');

// Вывод:
// 1
// A
// 2
// B

console.log('B') выполняется после console.log('2'), потому что после await продолжение функции уходит в Microtask Queue, а синхронный код (console.log('2')) выполняется сначала.


Типичные ошибки при работе с async/await

1. Забытый await

Одна из самых частых ошибок — работать с промисом как с обычным значением.

code
async function getUser() {
  return { name: 'Alice' };
}

async function main() {
  const user = getUser(); // Забыли await!
  console.log(user.name); // undefined — user это Promise, а не объект
}

Исправление очевидно, но TypeScript или ESLint с правилом no-floating-promises помогут поймать такие ошибки автоматически.

2. Последовательное ожидание вместо параллельного

code
// Медленно: запросы выполняются последовательно
async function slow() {
  const user = await fetchUser(1);    // ждём ~200ms
  const posts = await fetchPosts(1);  // ждём ещё ~200ms
  return { user, posts };             // итого ~400ms
}

// Быстро: запросы выполняются параллельно
async function fast() {
  const [user, posts] = await Promise.all([
    fetchUser(1),
    fetchPosts(1)
  ]);
  return { user, posts };             // итого ~200ms
}

Если задачи не зависят друг от друга, Promise.all — правильный выбор. Последовательный await оправдан только тогда, когда результат одной операции нужен для запуска следующей.

3. Неправильная обработка ошибок

code
// Проблема: ошибка внутри промиса не поймается
async function riskyOperation() {
  const result = await fetch('/api/data'); // может выбросить ошибку
  return result.json();
}

// Правильно: оборачивать в try/catch
async function safeOperation() {
  try {
    const result = await fetch('/api/data');
    return await result.json();
  } catch (error) {
    console.error('Запрос не удался:', error);
    return null;
  }
}

Важный нюанс: return result.json() без await внутри try/catch — потенциальная ловушка. Если result.json() выбросит ошибку, catch её не поймает, потому что функция уже вернула промис до того, как он был отклонён.

4. async в forEach

code
const ids = [1, 2, 3];

// Это не работает так, как ожидается
ids.forEach(async (id) => {
  const user = await fetchUser(id);
  console.log(user);
});

console.log('Готово'); // Выведется раньше пользователей!

forEach не ждёт асинхронных колбэков. Используйте for...of или Promise.all с .map:

code
// Вариант 1: последовательно
for (const id of ids) {
  const user = await fetchUser(id);
  console.log(user);
}

// Вариант 2: параллельно
const users = await Promise.all(ids.map(id => fetchUser(id)));
users.forEach(user => console.log(user));

5. Необработанные отклонения промисов

code
// Это создаст UnhandledPromiseRejection
async function fail() {
  throw new Error('Что-то пошло не так');
}

fail(); // промис отклонён, но никто не обрабатывает ошибку

В Node.js начиная с версии 15 необработанные отклонения промисов завершают процесс. В браузере они вызывают событие unhandledrejection. Всегда либо await промис внутри try/catch, либо добавляйте .catch().


Порядок выполнения: практический тест

Понимание Event Loop лучше всего проверяется на задачах с предсказанием вывода. Вот классический пример:

code
console.log('start');

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

Promise.resolve()
  .then(() => {
    console.log('promise 1');
    setTimeout(() => console.log('timeout 2'), 0);
  })
  .then(() => console.log('promise 2'));

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

console.log('end');

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

Вывод:

code
start
end
promise 1
promise 2
timeout 1
timeout 3
timeout 2

Разбор: синхронный код выполняется первым (start, end). Затем — все микрозадачи: promise 1, а внутри его обработчика добавляется timeout 2 в Macrotask Queue. Цепочка .then продолжается: promise 2. После полного опустошения Microtask Queue — макрозадачи в порядке добавления: timeout 1, timeout 3, timeout 2.

Именно такие задачи часто встречаются на технических собеседованиях в компании, работающие с Node.js или фронтендом. Тем, кто только начинает разбираться с асинхронностью, может быть полезен структурированный курс — например, «JavaScript – для начинающих!» от sudo teach IT, где эти концепции объясняются с нуля и с практическими упражнениями.


Несколько слов о Promise.allSettled, Promise.race и Promise.any

Помимо Promise.all, в арсенале есть другие комбинаторы:

  • Promise.allSettled — ждёт завершения всех промисов, независимо от того, выполнились они или отклонились. Полезно, когда нужно обработать все результаты, даже частично неуспешные.
  • Promise.race — разрешается или отклоняется, как только первый промис из массива завершится. Используется для реализации таймаутов.
  • Promise.any — разрешается, когда хотя бы один промис выполнился успешно. Отклоняется только если все промисы отклонились.
code
// Таймаут для fetch с Promise.race
function fetchWithTimeout(url, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([fetch(url), timeout]);
}

Вывод

async/await — удобный инструмент, но его поведение определяется механикой Event Loop, которая работает независимо от синтаксиса. Понимание разницы между микро- и макрозадачами, знание того, что происходит после каждого await, и осознание ловушек вроде параллельных запросов или async в forEach — это то, что отличает разработчика, который просто использует асинхронный код, от того, кто его контролирует.

Если вы хотите выстроить эти знания системно — от базовых типов и функций до промисов и асинхронности — курс «JavaScript – для начинающих!» может стать хорошей отправной точкой: он охватывает язык последовательно, без пропусков в фундаменте.

Асинхронный JavaScript перестаёт быть магией ровно в тот момент, когда вы понимаете, что Event Loop — это просто очень дисциплинированный диспетчер задач. Дальше всё становится предсказуемым.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

JavaScript – для начинающих!

Курс по теме

JavaScript – для начинающих!

С нуля до уверенной работы с JavaScript. Подходит для начинающих и тех, кто уже пишет код. Разберитесь с JS раз и навсегда — и перестаньте гуглить базовые вещи.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Как устроен GIL в Python: что он ограничивает и как это влияет на реальный код
устроен

Как устроен GIL в Python: что он ограничивает и как это влияет на реальный код

Разберём, почему GIL мешает CPU-bound потокам, а для IO-bound иногда всё “выглядит как будто работает”. Поймём, когда выбирать потоки, процессы и asyncio, и как измерить эффект на вашей задаче.

16 сентября 2026 г.
1020
Как устроен сборщик мусора в CPython: трассировка объектов и циклические ссылки
устроен

Как устроен сборщик мусора в CPython: трассировка объектов и циклические ссылки

Погружаемся в механизм reference counting и cyclic garbage collector — смотрим на исходный код CPython, пишем тесты и выясняем, когда объект действительно удаляется из памяти.

17 июля 2026 г.
790
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Однопоточность и проблема блокировкиАрхитектура: Call Stack, Web APIs и очереди задачCall StackWeb APIsОчереди задач: Macrotask Queue и Microtask QueueEvent Loop: как это всё связаноБесконечная очередь микрозадач — реальная опасностьasync/await: синтаксический сахар над промисамиЧто происходит под капотомТипичные ошибки при работе с async/await1. Забытый await2. Последовательное ожидание вместо параллельного3. Неправильная обработка ошибок4. async в forEach5. Необработанные отклонения промисовПорядок выполнения: практический тестНесколько слов о Promise.allSettled, Promise.race и Promise.anyВывод