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.
Theory questions
The Iterator Protocol
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.
Generator Functions vs Regular Functions
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.
Generators for Async Flow: The Bridge to `async`/`await`
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.
`yield*` Delegation
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.
Coding practice
Infinite Sequences with Generators
Implement and check your solution in the stated environment before revealing the answer.
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 sequence0n, 1n, 1n, 2n, 3n, 5n, .... Usebigintarithmetic so larger terms stay exact. This is an intentional numeric choice instead of eventually roundednumbervalues.- 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 mostnvalues from a synchronous iterable. A finite iterable may end sooner.nmust be a nonnegative safe-integer number. Invalid counts throwRangeErrorwhen consumption begins, not when the generator object is created. There is no count coercion.- For
n = 0, finish without readingSymbol.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
ncalls to the source'snext(). When the consumer resumes past the nth yielded value, finish and make the normal iterator-closing request, calling the source'sreturn()once if present. An early consumerbreakalso requests closing of an active source. Do not drain cleanup values. A generator that yields insidefinallycan 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.