Таймеры в Node.js: setTimeout, setInterval, setImmediate

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».

Таймеры выглядят одинаково в браузере и в Node, но у Node-версии есть свои особенности, которые то и дело удивляют: почему скрипт не выходит, почему setTimeout(0) не «сразу» и что за объект возвращается.

Четыре способа отложить код

Функция Когда выполнится Для чего
setTimeout(fn, ms) не раньше ms мс отложить на время
setInterval(fn, ms) каждые ms мс повторять
setImmediate(fn) в фазе check цикла «после текущего ввода-вывода»
process.nextTick(fn) сразу после текущей операции внутреннее, редко нужно

Порядок между ними задаёт событийный цикл, и он не совпадает с порядком в коде — это отдельная важная тема.

setTimeout — минимум, а не точность

setTimeout(() => console.log('через ~1 секунду'), 1000);

Ключевое: ms — это минимальная задержка. Node гарантирует, что колбэк не выполнится раньше указанного времени, но не гарантирует, что точно в срок. Если событийный цикл занят чужим колбэком или синхронным кодом, таймер подождёт.

Отсюда правило: таймеры не годятся для точного времени. Анимация, музыка, точные интервалы измерений — не их задача. Они хороши для «примерно через столько» — таймауты, повторные попытки, опрос.

И setTimeout(fn, 0) не значит «немедленно»: ноль — это «как только цикл дойдёт до фазы timers», а до неё успеет отработать вся очередь микрозадач.

Таймер возвращает объект, а не число

Отличие от браузера, на котором спотыкаются:

const id = setTimeout(() => {}, 1000);
console.log(typeof id);   // "object" в Node, "number" в браузере

clearTimeout(id);         // отменяем — работает и там, и там

В браузере таймер возвращает числовой идентификатор, в Node — объект Timeout. Обычно разница незаметна (обе версии принимает clearTimeout), но у Node-объекта есть полезные методы, которых нет у числа.

unref: почему скрипт не завершается

Самая частая практическая проблема с таймерами. Активный таймер удерживает процесс от завершения — Node считает, что раз есть запланированная работа, выходить рано.

// этот скрипт НЕ завершится сам — интервал держит его живым
setInterval(() => console.log('тик'), 1000);

Два решения:

// 1. снять таймер, когда он больше не нужен
const id = setInterval(fn, 1000);
clearInterval(id);

// 2. сказать «не держи процесс ради меня»
const timer = setTimeout(fn, 60000);
timer.unref();   // процесс завершится, даже если таймер ещё тикает

unref() снимает у таймера право удерживать процесс: он продолжит работать, но не помешает Node выйти, если больше делать нечего. Обратная операция — ref().

Это ровно тот случай, о котором мы говорили в разборе глобальных объектов: «скрипт отработал, а процесс висит» — почти всегда забытый живой интервал или незакрытый сервер.

setImmediate против setTimeout(0)

Тонкое, но иногда важное различие:

setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
  • внутри колбэка ввода-вывода (например, после чтения файла) setImmediate всегда выполнится раньше — он в следующей фазе check, а timers будет только на следующем обороте;
  • на верхнем уровне порядок между ними не гарантирован и зависит от того, как быстро стартовал цикл.

Практический вывод: если нужно «выполнить после текущей операции ввода-вывода, но до таймеров» — это setImmediate. Полагаться на порядок setTimeout(0) не стоит.

Промис-версии: паузы без колбэков

Современный способ сделать паузу в async-коде — модуль node:timers/promises:

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

console.log('начали');
await sleep(1000);          // пауза без колбэка и без new Promise
console.log('прошла секунда');

// с возвращаемым значением
const value = await sleep(100, 'готово');   // через 100 мс вернёт 'готово'

Раньше для паузы оборачивали setTimeout в new Promise вручную — теперь это одна строка. Там же есть промис-версии setInterval (как асинхронный итератор) и setImmediate.

И бонус — отмена через AbortSignal:

await sleep(5000, undefined, { signal: AbortSignal.timeout(1000) });
// прервётся через 1 секунду с AbortError

setInterval и дрейф

Один нюанс про повторяющиеся таймеры. setInterval не гарантирует ровный интервал: если колбэк выполняется дольше, чем период, вызовы наслаиваются или сдвигаются. Для задач, где важна регулярность, надёжнее рекурсивный setTimeout — планировать следующий вызов в конце текущего:

// вместо setInterval при длинной работе
async function loop() {
  await doWork();
  setTimeout(loop, 1000);   // следующий запуск — после завершения текущего
}
loop();

Смежные темы: событийный цикл — почему такой порядок; промисы — async-паузы; глобальные объекты — таймеры без импорта. Полный список — в справочнике по Node.js.

Частые вопросы

Точен ли setTimeout?
Нет. Задержка — это минимум, а не гарантия: колбэк выполнится не раньше указанного времени, но может и позже, если событийный цикл занят. Для точного времени таймеры не подходят.
Чем setImmediate отличается от setTimeout(0)?
setImmediate выполняется в фазе check событийного цикла, setTimeout(0) — в фазе timers. Внутри колбэка ввода-вывода setImmediate гарантированно раньше; на верхнем уровне порядок между ними не определён.
Что такое unref у таймера?
Метод, который говорит: этот таймер не должен удерживать процесс от завершения. Обычно живой таймер не даёт Node выйти. С unref() процесс завершится, даже если таймер ещё тикает.
Почему скрипт не завершается сам?
Чаще всего из-за забытого setInterval или незакрытого сервера: пока есть активный таймер или соединение, Node считает, что работа не закончена. Решение — clearInterval или unref().
Есть ли промис-версия таймеров?
Да, модуль node:timers/promises. await setTimeout(1000) из него — это пауза без колбэка. Удобно для задержек в async-коде вместо обёртки setTimeout в new Promise.