Урок по треку Node.js и бэкенд 7 мин

Асинхронные паттерны в Node.js

Колбэки, промисы, async/await, ограничение параллелизма, отмена через AbortController, EventEmitter.

В Node почти любой ввод-вывод асинхронный. За годы сменились три стиля: колбэки, промисы и async/await. На собесе проверяют, умеешь ли ты запускать операции параллельно, ограничивать их число, отменять и не терять ошибки.

От колбэков к async/await

Исторический стиль Node это error-first callback: первым аргументом приходит ошибка, вторым результат.

fs.readFile('a.txt', 'utf8', (err, data) => {
  if (err) return handle(err);
  use(data);
});

Вложенные колбэки быстро превращаются в «пирамиду», а ошибки легко забыть. Колбэк-API превращают в промисы через util.promisify, но у большинства модулей уже есть промис-версия: node:fs/promises, node:timers/promises, node:stream/promises.

async-функция всегда возвращает промис. await приостанавливает только эту функцию, а не весь процесс: остальной код и другие запросы продолжают работать. Тело до первого await выполняется синхронно.

Последовательно или параллельно

// последовательно: 200 мс
const a = await sleep(100);
const b = await sleep(100);

// параллельно: 100 мс
const [a, b] = await Promise.all([sleep(100), sleep(100)]);

Частая ошибка: ставить await в цикл там, где запросы независимы. Обратная ошибка: array.forEach(async ...). forEach не ждёт промисы, поэтому код после цикла выполнится раньше, а ошибки улетят в unhandled rejection. Для последовательной обработки нужен for...of с await, для параллельной Promise.all(array.map(...)).

Комбинаторы промисов

МетодКогда выполнитсяКогда отклонится
Promise.allвсе успешны (массив результатов)при первой ошибке
Promise.allSettledкогда все завершились (статусы)никогда
Promise.raceпервый завершившийся, успешнопервый завершившийся с ошибкой
Promise.anyпервый успешныйвсе упали: AggregateError

Promise.all при ошибке не отменяет остальные операции: они продолжают выполняться, просто их результат никто не ждёт.

Ограничение параллелизма

Promise.all на 10 000 URL откроет 10 000 соединений разом: упрёшься в лимиты API, файловых дескрипторов или пула БД. Нужен пул: одновременно не больше N задач.

async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i]);
    }
  }
  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}

В проде то же самое дают пакеты вроде p-limit или параметр конкурентности у очередей.

Отмена и таймауты

Промис нельзя отменить сам по себе. Стандартный механизм это AbortController: операция принимает signal и прерывается, когда вызывают abort().

const res = await fetch(url, { signal: AbortSignal.timeout(3000) });

AbortSignal.timeout(ms) прерывает с ошибкой TimeoutError, ручной abort() даёт AbortError. Сигнал принимают fetch, fs/promises, timers/promises, events.once, stream.pipeline. Promise.race с таймером даёт только видимость таймаута: исходная операция продолжает работать.

EventEmitter

Многие объекты Node (сервер, потоки, сокеты) это EventEmitter. Важно:

  • emit вызывает слушателей синхронно, по порядку подписки.
  • once срабатывает один раз.
  • Событие error особое: если у эмиттера нет слушателя error, emit('error') бросит исключение и обычно уронит процесс.
  • Больше 10 слушателей одного события дают предупреждение о возможной утечке: часто это подписка в цикле или на каждый запрос без отписки.
  • await once(emitter, 'event') из node:events превращает событие в промис.

Частые ошибки

  • await в цикле для независимых операций.
  • forEach(async ...) и ожидание, что цикл дождётся.
  • Promise.all без ограничения на тысячи задач.
  • return promise внутри try без await: ошибка не попадёт в catch этой функции.
  • Забытый await: ошибка превращается в unhandled rejection.

Что спросят на собесе

  • Как запустить запросы параллельно и чем отличаются all, allSettled, race, any.
  • Как ограничить число одновременных запросов.
  • Как отменить запрос или сделать таймаут.
  • Почему forEach с async не работает как ожидается.
  • Как работает EventEmitter и что особенного в событии error.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (29)