Привет, Хабр!
Сегодня мы рассмотрим один из тех маленьких, но мощных апгрейдов 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:
Лучше читается (сигнализирует намерение — пауза, а не Promise‑хак);
Поддерживает
AbortSignal, что особенно важно в пайплайнах;Надёжно работает в
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, чтобы узнать, достаточно ли ваших знаний для поступления на курс.
