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