Search
Write a publication
Pull to refresh
605.78
OTUS
Развиваем технологии, обучая их создателей

timers.promises в Node.js

Level of difficultyEasy
Reading time4 min
Views1.5K

Привет, Хабр!

Сегодня мы рассмотрим один из тех маленьких, но мощных апгрейдов Node.js, который вы, скорее всего, недооценивали. Речь о timers.promises — свежем и способе работать с setTimeout и setImmediate в асинхронных функциях.

setTimeout и setImmediate как промисы

В timers/promises есть два метода:

import { setTimeout, setImmediate } from 'node:timers/promises';

setTimeout

Простейший пример:

await setTimeout(2000);
console.log('2 секунды прошли');

Также можно вернуть значение:

const result = await setTimeout(1000, 'Hello after 1s');
console.log(result); // Hello after 1s

Можно передать любой value, который вернётся промисом. Для долгих или отменяемых операций — просто золото.

setImmediate

Это уже микрозадача уровня setImmediate:

await setImmediate();
console.log('Я выполнюсь сразу после текущего event loop');

Если сравнивать с process.nextTick, о чём ниже, setImmediate всё‑таки даёт системе глотнуть воздуха, а nextTick исполняется в том же цикле.

Отмена таймаутов с AbortSignal

timers/promises имеет ещё одну мощную фичу: поддержку AbortSignal.

import { setTimeout } from 'node:timers/promises';
import { AbortController } from 'node:abort-controller';

const controller = new AbortController();

setTimeout(5000, undefined, { signal: controller.signal })
  .then(() => console.log('не отменён'))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Таймаут отменён');
    } else {
      throw err;
    }
  });

setTimeout(2000).then(() => controller.abort());

В примере выше таймаут на 5 секунд отменяется через 2 секунды. AbortController нативный для Node.js и браузеров. В старых версиях Node ставим пакет abort-controller.

setTimeout против process.nextTick

Если вам нужна синхронная очередь микрозадач, то process.nextTick быстрее setTimeout(fn, 0) в сотни раз. Но это не всегда благо: nextTick может захватить event loop и не дать I/O возможности обработаться.

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));

Что выведется?

nextTick
immediate
timeout

process.nextTick всегда лезет первым, что в проде может быть антипаттерном, если им увлекаться. В 99% случаев вместо nextTick лучше setImmediate, если надо сделать «позже, но не сильно позже».

Дебаунс и троттлинг с промисными таймерами

Дебаунс:

function debounce(fn, delay = 300) {
  let timeoutId;
  return (...args) => {
    if (timeoutId) timeoutId.abort();
    const controller = new AbortController();
    timeoutId = controller;
    setTimeout(delay, undefined, { signal: controller.signal })
      .then(() => fn(...args))
      .catch(() => {});
  };
}

const log = debounce(msg => console.log(msg), 500);

log('A');
log('B');
log('C');
// В консоли только 'C'

Мы используем AbortSignal для отмены предыдущего таймера — красиво и нативно.

Троттлинг:

function throttle(fn, limit = 300) {
  let lastRun = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastRun >= limit) {
      lastRun = now;
      fn(...args);
    }
  };
}

const logThrottle = throttle(msg => console.log(msg), 1000);

setInterval(() => logThrottle('tick'), 200);

throttle — более прямолинейный, тут промисы не нужны.

Как тестировать асинхронные таймеры

Для юнит‑тестов есть отличный паттерн: мокать таймеры с помощью sinon или встроенного jest.useFakeTimers(). Пример на Jest:

import { setTimeout } from 'node:timers/promises';

jest.useFakeTimers();

test('ждём таймаут', async () => {
  const spy = jest.fn();
  const promise = setTimeout(1000).then(spy);

  jest.advanceTimersByTime(1000);

  await promise;

  expect(spy).toHaveBeenCalled();
});

afterAll(() => {
  jest.useRealTimers();
});

AbortSignal тоже можно мокать и дергать его метод abort() в нужный момент — так вы покроете и happy‑path, и early‑cancel‑path. Так что не забрасывайте тестами такие мелочи, как таймеры — они выстрелят ровно тогда, когда отвалится SLA.

Таймеры в цепочке: setTimeout как ограничитель повтора

Иногда нужно вставить в async‑цепочку задержку, чтобы разгрузить внешнюю систему, но не останавливать логику полностью.

Старая школа:

for (const item of items) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  await processItem(item);
}

Новая школа:

import { setTimeout } from 'node:timers/promises';

for (const item of items) {
  await setTimeout(1000);
  await processItem(item);
}

Казалось бы, разницы мало. Но setTimeout из timers/promises:

  1. Лучше читается (сигнализирует намерение — пауза, а не Promise‑хак);

  2. Поддерживает AbortSignal, что особенно важно в пайплайнах;

  3. Надёжно работает в try/catch, без лишнего обвеса.

Теперь пример с отменой:

const controller = new AbortController();

async function processItems(items) {
  for (const item of items) {
    await setTimeout(1000, undefined, { signal: controller.signal });
    await processItem(item);
  }
}

// Прервать выполнение по таймеру
setTimeout(() => controller.abort(), 5000);

Заключение

timers.promises — это именно та малая деталь, которая отделяет «написали, лишь бы работало» от «сделали чисто, красиво и безопасно». В современных async/await сценариях от него нет смысла отказываться: меньше ручного кода, выше читаемость, встроенная поддержка отмены и адекватное поведение в больших пайплайнах.

Кроме того, промисные таймеры открывают путь к аккуратным и понятным реализациям распространённых паттернов вроде дебаунса, троттлинга, таймаутов на операции, backoff‑стратегий. И при этом они уже входят в стандартную библиотеку Node.js, протестированы и поддерживаются, так что не нужно городить велосипед на базе new Promise или тянуть сторонние пакеты.

Если у вас есть свой опыт по использованию timers.promises, рабочие паттерны или нюансы — делитесь в комментариях.


Если вы работаете с Node.js и хотите глубже понять современные подходы к разработке API и микросервисов, приглашаем вас на серию открытых уроков:

24 июля в 20:00 — Создаём масштабируемый микросервис с Nest.js и Kubernetes
4 августа в 20:00 — Как создать API‑сервер с TypeScript и Node.js
14 августа в 20:00 — Пишем высоконагруженное отказоустойчивое API на Bun и Elysia

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

Кроме того, вы можете пройти тест по курсу Node.js Developer, чтобы узнать, достаточно ли ваших знаний для поступления на курс.

Tags:
Hubs:
+6
Comments0

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS