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