Backend

Как Node.js выполняет асинхронный код на одном потоке

Владислав Анохин
Владислав Анохин30 окт. 2026
Обложка со схемой потока: логотип Node.js, цепочка из таймера, цикла Event Loop и сервера на оранжевом маршруте запроса

Асинхронность в Node.js объясняют одной фразой: неблокирующий ввод-вывод. За ней стоит механика, от которой зависит поведение сервера. Почему тяжёлый синхронный цикл останавливает все запросы? Дальше - Event Loop по шагам, на примерах из LTS-версии.

Почему асинхронность в Node.js не про потоки

По умолчанию JavaScript выполняется в одном потоке: инструкции идут строго по очереди. Ввод-вывод при этом идёт параллельно: операция уходит ядру ОС, а колбэк попадает в очередь, когда ядро сообщает о результате.

Блокирующей считают операцию, из-за которой поток ждёт результат и не берёт следующую задачу. Типичный пример - синхронные методы с суффиксом Sync. Подробнее в руководстве Node.js по блокирующим вызовам.

Когда вызов блокирует процесс, а когда нет

Асинхронный readFile отдаёт управление сразу: пока диск отвечает, поток обслуживает другие запросы. readFileSync возвращает данные в той же строке, но до конца чтения процесс не делает ничего. Под нагрузкой он задерживает всех остальных.

Как Event Loop проходит свои фазы

За одну итерацию Event Loop проходит фазы по порядку: timers, pending callbacks, idle и prepare, poll, check и close callbacks. У каждой своя очередь колбэков. Порядок фаз описан в документации по Event Loop.

Схема фаз Event Loop в Node.js: timers, pending callbacks, idle, prepare, poll, check и close callbacks идут по кругу, фаза poll выделена

Что решает фаза poll

Фаза poll ждёт ввод-вывод и разбирает свою очередь. Если очередь пуста, а setImmediate() запланирован, цикл переходит к check.

Порядок выполнения: nextTick, промисы и setImmediate

В CommonJS сначала разбирается process.nextTick, затем микротаски промисов, и только потом макротаски вроде setTimeout и setImmediate. В ESM первые две меняются местами. Очередь nextTick формально не входит в Event Loop: её разбирают после текущей операции, в какой бы фазе цикл ни стоял.

Пример - в CommonJS:

const baz = () => console.log('baz');
const foo = () => console.log('foo');
const zoo = () => console.log('zoo');

const start = () => {
  console.log('start');
  setImmediate(baz);
  new Promise((resolve) => resolve('bar')).then((value) => {
    console.log(value);
    process.nextTick(zoo);
  });
  process.nextTick(foo);
};

start();
// start foo bar zoo baz

Сначала печатается start, затем nextTick выводит foo, а промис - bar. Колбэк zoo ставится в nextTick уже из микротаски, а baz из setImmediate ждёт фазы check и идёт последним.

Один и тот же код в CJS и ESM даёт разный порядок

Тот же код в .mjs выведет start bar foo zoo baz. ESM-модуль исполняется внутри микротаск-очереди, поэтому промис срабатывает раньше nextTick.

Рекурсивный nextTick - ловушка: Event Loop не доходит до poll, и ввод-вывод может заглохнуть. С версий v22.7.0 и v20.18.0 документация помечает process.nextTick как Legacy и рекомендует queueMicrotask(). Сам API при этом не удалён.

setTimeout и setImmediate: где порядок определён

Если оба вызова стоят внутри колбэка ввода-вывода, setImmediate срабатывает раньше таймера всегда. В главном модуле порядок setTimeout(fn, 0) и setImmediate недетерминирован и зависит от производительности процесса.

const fs = require('node:fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});
// immediate
// timeout

Колбэк readFile выполняется в фазе poll, поэтому setImmediate попадает в check раньше таймера. Вынесите оба вызова в главный модуль, и гарантии не будет.

Таймер задаёт порог, а не точное время. Задержку увеличивают планировщик ОС и другие колбэки.

Что делает await, если поток не блокируется

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

Async-функции из ES2017 появились в V8 5.5 (октябрь 2016), а в Node.js - с обновлением V8 в версии 7.6.0.

Ошибки и отмена: unhandledRejection и AbortSignal

Отклонённый промис без обработчика к концу текущей итерации Event Loop вызывает событие unhandledRejection. С Node.js 15.0.0 режим по умолчанию - throw: необработанное отклонение роняет процесс. Как это доходит до обработчика в Express - в статье про асинхронные обработчики в Express.

Для отмены есть AbortSignal.timeout(ms): сигнал срабатывает через ms миллисекунд. Появился в v17.3.0 и v16.14.0.

Promise.all, allSettled и any: как не ждать по одному

Цикл с await запускает операции по очереди: каждая ждёт предыдущую. Promise.all стартует все сразу. Если операции независимы, общее время равно самой долгой из них.

Пример для Linux и macOS, на Windows подставьте пути. Файл - .mjs: нужны ESM и top-level await.

import { readFile } from 'node:fs/promises';

const files = ['/etc/hostname', '/etc/hosts'];

// параллельно
const texts = await Promise.all(files.map((file) => readFile(file, 'utf8')));

map возвращает массив промисов, Promise.all дожидается всех и отдаёт массив текстов.

Если одна из операций отклонится, весь Promise.all отклонится тоже. Здесь проходит граница с allSettled: Promise.allSettled дожидается всех промисов и возвращает исход каждого. Promise.any, наоборот, отдаёт первый успешный результат.

EventEmitter и синхронные слушатели

Значительная часть ядра Node.js построена на событиях, а такие объекты - экземпляры EventEmitter. Слушатели вызываются синхронно, в порядке регистрации, а возвращаемые значения игнорируются. Поэтому async-слушатель не заставит emit ждать свой промис.

Почему событие error без слушателя роняет процесс

Если у эмиттера нет слушателя 'error', ошибка выбрасывается, печатается стек и процесс завершается. Опция captureRejections перехватывает отклонения в async-слушателях, но по умолчанию выключена.

Тот же Event Loop обслуживает и WebSockets, и Nest.js.

📩 Нужна команда под задачу? Посмотрите, как мы ведём проекты: оставить заявку.

Читайте также

Обложка со схемой потока: логотип Express.js, серверный куб с логотипом Node.js, карточки ответов на оранжевом маршруте и окно браузера
Backend

Как собрать REST API на Express.js за вечер

Собираем REST API на Express.js: маршрутизация, middleware, обработка ошибок и подключение к PostgreSQL. Проверенные примеры кода для Express 5 и Node.js 24 LTS.

Владислав Анохин
Владислав Анохин
21 окт. 2026
WebSockets: протокол двусторонней связи в реальном времени между браузером и сервером - изометрическая иллюстрация с потоком данных
Backend

WebSocket: как работает реальное время в вебе

WebSocket - протокол связи, который позволяет серверу и клиенту обмениваться данными в реальном времени через одно постоянное соединение. В статье разбираем устройство протокола, сравниваем с HTTP и реализуем практические примеры на Node.js: echo-сервер, broadcast и чат с rooms через Socket.IO.

Владислав Анохин
Владислав Анохин
24 апр. 2026
NestJS
Backend

Nest.js: современный backend-фреймворк для Node.js

Nest.js — современный backend-фреймворк для Node.js с TypeScript. Разбираем архитектуру, модули, внедрение зависимостей и создание REST API. Практические примеры и сравнение с Express.js помогут выбрать лучший инструмент для вашего проекта. Узнайте, почему крупные компании выбирают Nest.js для масштабируемых приложений.

Владислав Анохин
Владислав Анохин
04 апр. 2026