Lesson overview · Free interview practice

Generators

Explore iterators and generators for flow control.

Topics in the full lesson
  • Introduction to Iterables and Iterators
  • Creating Custom Iterators
  • Generator Functions
  • Advanced Generator Techniques
  • Using Generators for Asynchronous Control Flow
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryEasy

The Iterator Protocol

Question

What is the JavaScript iterator protocol, and how do you make an object iterable without using a generator?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

Generator Functions vs Regular Functions

Question

How does a generator function differ from a regular function? Show the canonical "pause-and-resume" mechanic.

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

Generators for Async Flow: The Bridge to `async`/`await`

Question

How were generators used for async control flow before async/await shipped? Why does understanding this history help you debug async/await today?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

`yield*` Delegation

Question

What does yield* do, and when is it more than just a shortcut?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingEasy

Infinite Sequences with Generators

Implement and check your solution in the stated environment before revealing the answer.

Question

How do generators make infinite sequences practical? Implement fibonacci() and take(iterable, n) so you can safely collect just the first N values.

Core practice. The lesson already teaches infinite producers and yield. Start with two calls to .next(), then add bounded consumption. The TypeScript version is optional, not a different required exercise.

Contract

  • fibonacci() returns a fresh lazy generator with the sequence 0n, 1n, 1n, 2n, 3n, 5n, .... Use bigint arithmetic so larger terms stay exact. This is an intentional numeric choice instead of eventually rounded number values.
  • Each generator has independent state. It has no built-in final term. Do not precompute an array or attempt to exhaust it.
  • take(iterable, n) returns a lazy generator of at most n values from a synchronous iterable. A finite iterable may end sooner.
  • n must be a nonnegative safe-integer number. Invalid counts throw RangeError when consumption begins, not when the generator object is created. There is no count coercion.
  • For n = 0, finish without reading Symbol.iterator, opening the source or pulling a value. Validate the count before touching the source.
  • For positive counts, use normal iteration and make at most n calls to the source's next(). When the consumer resumes past the nth yielded value, finish and make the normal iterator-closing request, calling the source's return() once if present. An early consumer break also requests closing of an active source. Do not drain cleanup values. A generator that yields inside finally can remain suspended after that request.
  • Propagate source errors unchanged. For a positive count, non-iterable input fails through normal iteration with TypeError. Async iterables and malformed custom iterator protocols are outside scope.

[...take(fibonacci(), 6)] should be [0n, 1n, 1n, 2n, 3n, 5n]. [...take(fibonacci(), 0)] should be [].

Never spread fibonacci() directly. For this practice, every consumption of the infinite producer must be bounded.

Set up an attempt

Use Node.js 24.21.0. Work in a small local folder, save the files below exactly as named, then edit only the starter. The .mjs extension makes the JavaScript files ES modules. No package install, browser, network request or project framework is needed.

Run the visible checks from that folder:

node check.mjs

The supplied starter intentionally throws an Implement ... error. That first failure confirms that the checker reaches your code. Work toward the named success message at the end of the check, not toward hiding assertions.

These are finite checks of the stated cases, not a proof of all possible correctness or mastery. Be ready to explain the behavior as well as run it.

starter.mjs

export function* fibonacci() {
  throw new Error('Implement fibonacci')
}

export function* take(iterable, n) {
  throw new Error('Implement take')
}

check.mjs

import assert from 'node:assert/strict'
const { fibonacci, take } = await import(new URL(process.argv[2] ?? './starter.mjs', import.meta.url))

assert.deepEqual([...take(fibonacci(), 6)], [0n, 1n, 1n, 2n, 3n, 5n])
const fib = fibonacci()
assert.deepEqual(fib.next(), { value: 0n, done: false })
assert.deepEqual(fib.next(), { value: 1n, done: false })
assert.equal(fibonacci().next().value, 0n)
assert.equal([...take(fibonacci(), 101)].at(-1), 354224848179261915075n)
assert.deepEqual([...take([10, 20], 9)], [10, 20])
assert.deepEqual([...take([], 4)], [])
assert.deepEqual([...take('abc', 2)], ['a', 'b'])
const noTouch = {
  get [Symbol.iterator]() { throw new Error('must not read the iterator') },
}
assert.deepEqual([...take(noTouch, 0)], [])
for (const n of [-1, 1.5, NaN, Infinity, '2', Number.MAX_SAFE_INTEGER + 1]) {
  const output = take(noTouch, n)
  assert.throws(() => output.next(), RangeError)
}
assert.deepEqual([...take([], Number.MAX_SAFE_INTEGER)], [])

let pulled = 0
let closed = 0
function* source() {
  try {
    while (true) {
      pulled += 1
      yield pulled
    }
  } finally {
    closed += 1
  }
}
const limited = take(source(), 2)
assert.equal(pulled, 0)
assert.deepEqual(limited.next(), { value: 1, done: false })
assert.deepEqual(limited.next(), { value: 2, done: false })
assert.equal(closed, 0)
assert.equal(limited.next().done, true)
assert.equal(pulled, 2)
assert.equal(closed, 1)
for (const value of take(source(), 10)) {
  assert.equal(value, 3)
  break
}
assert.equal(closed, 2)
const original = fibonacci()
assert.deepEqual([...take(original, 2)], [0n, 1n])
assert.equal(original.next().done, true)
const cleanupTrace = []
function* yieldingCleanup() {
  try {
    yield 'data'
  } finally {
    cleanupTrace.push('started')
    yield 'cleanup 1'
    yield 'cleanup 2'
    cleanupTrace.push('finished')
  }
}
const cleanupSource = yieldingCleanup()
const originalReturn = cleanupSource.return.bind(cleanupSource)
let closeRequests = 0
cleanupSource.return = (value) => {
  closeRequests += 1
  return originalReturn(value)
}
assert.deepEqual([...take(cleanupSource, 1)], ['data'])
assert.equal(closeRequests, 1)
assert.deepEqual(cleanupTrace, ['started'])
assert.deepEqual(cleanupSource.next(), { value: 'cleanup 2', done: false })
assert.deepEqual(cleanupSource.next(), { value: undefined, done: true })
assert.equal(closeRequests, 1)
assert.deepEqual(cleanupTrace, ['started', 'finished'])
const marker = new Error('source failed')
function* failing() {
  yield 1
  throw marker
}
assert.throws(() => [...take(failing(), 3)], (error) => error === marker)
assert.throws(() => [...take(null, 1)], TypeError)

function* map(iterable, fn) {
  for (const value of iterable) yield fn(value)
}
function* filter(iterable, predicate) {
  for (const value of iterable) if (predicate(value)) yield value
}
const pipeline = take(filter(map(fibonacci(), (n) => n * 2n), (n) => n > 10n), 3)
assert.deepEqual([...pipeline], [16n, 26n, 42n])
console.log('generators checks passed')

Optional TypeScript attempt

Use this instead of the JavaScript starter if you want a typed implementation. Save starter.mts, types.mts and tsconfig.json below beside the same check.mjs. The @ts-expect-error lines are intentional negative type checks. Their guarded block is not executed.

With an existing TypeScript 5.9.3 compiler available, run both commands:

tsc -p tsconfig.json
node check.mjs ./starter.mts

The first command performs a strict no-emit type check. The second runs the behavioral checks using Node's type stripping. Stripping is not type checking. The unimplemented typed starter should type-check but fail the runtime check until you implement it.

The configuration uses standard library declarations only. No @types/node package is needed for these small fixtures.

starter.mts

export function* fibonacci(): Generator<bigint, void, unknown> {
  throw new Error('Implement fibonacci')
}

export function* take<T>(iterable: Iterable<T>, n: number): Generator<T, void, unknown> {
  throw new Error('Implement take')
}

types.mts

import { fibonacci, take } from './starter.mts'

const values: bigint[] = [...take(fibonacci(), 6)]
const words: string[] = [...take(['hello', 'world'], 1)]
if (values[5] !== 5n || words[0] !== 'hello') throw new Error('Typed usage failed')
if (false) {
  // @ts-expect-error The sequence produces bigint, not number.
  const numbers: number[] = [...take(fibonacci(), 1)]
  // @ts-expect-error Counts are numbers.
  take(fibonacci(), '2')
}

tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "lib": [
      "ES2022",
      "DOM"
    ],
    "types": []
  },
  "files": [
    "starter.mts",
    "types.mts"
  ]
}

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Generators material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium