Generators (function*) produce values one at a time, only when asked. The most common use I have for them is generating unique IDs without keeping a counter floating around in module scope.

function* idGen(prefix = "id") {
  let i = 0;
  while (true) yield `${prefix}_${++i}`;
}

const nextId = idGen("user");
nextId.next().value; // "user_1"
nextId.next().value; // "user_2"
nextId.next().value; // "user_3"

The while (true) looks scary but itโ€™s fine, yield pauses the function and hands control back to the caller. The next .next() resumes it.

You can do the same for infinite sequences. Fibonacci as a one-screen example:

function* fib() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const f = fib();
[
  f.next().value,
  f.next().value,
  f.next().value,
  f.next().value,
  f.next().value,
];
// [0, 1, 1, 2, 3]

Generators are also iterable, so you can use them with for...of and spread ๐Ÿคฏ, just remember infinite ones will, well, never finish:

for (const id of idGen()) {
  if (id === "id_5") break;
  console.log(id);
}

Lazy, stateful, no globals.