Привет, Хабр!
Сегодня мы рассмотрим один из тех маленьких, но мощных апгрейдов 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, чтобы узнать, достаточно ли ваших знаний для поступления на курс.