JavaScript Basics: Consume Timer Ticks with an Async Iterator
A periodic local task can consume ticks with for await instead of registering a repeating callback. Node’s promise-based interval returns an async iterator. Stop a finite loop explicitly and verify a pre-aborted iterator rejects.
Prepare a scratch directory
Tested on Linux with Node.js v24.19.0. You need the named runtime, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. The .mjs extension selects ES modules. Assertions check the demonstrated behavior; no npm packages or remote services are needed.
Keep using the same terminal. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-interval-iterator.mjs:
import assert from 'node:assert/strict';
import { setInterval as every } from 'node:timers/promises';
const seen = [];
for await (const value of every(1, 'tick')) {
seen.push(value);
console.log(`Tick ${seen.length}: ${value}`);
if (seen.length === 3) break;
}
assert.deepEqual(seen, ['tick', 'tick', 'tick']);
console.log('Finite interval loop finished');
const controller = new AbortController();
controller.abort();
const cancelled = every(1, 'unused', { signal: controller.signal });
await assert.rejects(cancelled.next(), { name: 'AbortError' });
console.log('Pre-aborted interval: rejected');Run it:
node demonstrate-interval-iterator.mjsObserved output:
Tick 1: tick
Tick 2: tick
Tick 3: tick
Finite interval loop finished
Pre-aborted interval: rejectedRead the result
Each iteration receives the value supplied to setInterval. Breaking the loop closes the iterator and clears its interval, allowing this program to finish. The one-millisecond delay is deliberately not measured: scheduling depends on other work. The second case verifies rejection before any tick is consumed, with the rejection handled by assert.rejects.
Apply it carefully
This is an interval schedule, not “wait one delay after the previous job finishes.” When work is slower than the interval, ticks can accumulate and subsequent iterations may become immediately available. Use a timeout after each job when you need a delay between completed jobs. A default referenced interval keeps the process alive, so always provide a termination path. Cancellation is cooperative and cannot undo completed side effects.
The official reference describes the API and its options. The checks above cover the specific fixture, not every possible input or environment.
Clean up
Remove the source and then the empty practice directory:
rm -- demonstrate-interval-iterator.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect the remaining files rather than deleting unknown contents.
Acknowledgements
This post was created with assistance from AI.