Таймеры в 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 у таймера?
unref() процесс завершится, даже если таймер ещё тикает.Почему скрипт не завершается сам?
Есть ли промис-версия таймеров?
await setTimeout(1000) из него — это пауза без колбэка. Удобно для задержек в async-коде вместо обёртки setTimeout в new Promise.