Generators are functions that can pause and resume execution. Here's how to use them effectively.
Basic Generators#
1// Generator function syntax
2function* numberGenerator() {
3 yield 1;
4 yield 2;
5 yield 3;
6}
7
8const gen = numberGenerator();
9
10console.log(gen.next()); // { value: 1, done: false }
11console.log(gen.next()); // { value: 2, done: false }
12console.log(gen.next()); // { value: 3, done: false }
13console.log(gen.next()); // { value: undefined, done: true }
14
15// Using for...of
16for (const num of numberGenerator()) {
17 console.log(num); // 1, 2, 3
18}
19
20// Spread operator
21const numbers = [...numberGenerator()]; // [1, 2, 3]Yield Expressions#
1// yield returns values
2function* greetings() {
3 yield 'Hello';
4 yield 'World';
5 return 'Done'; // Return value only in final next()
6}
7
8const g = greetings();
9console.log(g.next()); // { value: 'Hello', done: false }
10console.log(g.next()); // { value: 'World', done: false }
11console.log(g.next()); // { value: 'Done', done: true }
12
13// yield expressions can receive values
14function* conversation() {
15 const name = yield 'What is your name?';
16 const age = yield `Hello ${name}, how old are you?`;
17 yield `${name} is ${age} years old`;
18}
19
20const conv = conversation();
21console.log(conv.next().value); // 'What is your name?'
22console.log(conv.next('Alice').value); // 'Hello Alice, how old are you?'
23console.log(conv.next(25).value); // 'Alice is 25 years old'Infinite Generators#
1// Infinite sequence
2function* infiniteCounter() {
3 let count = 0;
4 while (true) {
5 yield count++;
6 }
7}
8
9const counter = infiniteCounter();
10console.log(counter.next().value); // 0
11console.log(counter.next().value); // 1
12console.log(counter.next().value); // 2
13
14// Fibonacci sequence
15function* fibonacci() {
16 let [prev, curr] = [0, 1];
17 while (true) {
18 yield curr;
19 [prev, curr] = [curr, prev + curr];
20 }
21}
22
23const fib = fibonacci();
24console.log(fib.next().value); // 1
25console.log(fib.next().value); // 1
26console.log(fib.next().value); // 2
27console.log(fib.next().value); // 3
28console.log(fib.next().value); // 5
29
30// Take first n values
31function* take(iterable, n) {
32 let count = 0;
33 for (const item of iterable) {
34 if (count >= n) return;
35 yield item;
36 count++;
37 }
38}
39
40const first10Fib = [...take(fibonacci(), 10)];
41// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]Delegating Generators#
1// yield* delegates to another generator
2function* inner() {
3 yield 'a';
4 yield 'b';
5}
6
7function* outer() {
8 yield 1;
9 yield* inner();
10 yield 2;
11}
12
13console.log([...outer()]); // [1, 'a', 'b', 2]
14
15// yield* with return value
16function* innerWithReturn() {
17 yield 'x';
18 yield 'y';
19 return 'inner done';
20}
21
22function* outerWithReturn() {
23 const result = yield* innerWithReturn();
24 yield `Result: ${result}`;
25}
26
27console.log([...outerWithReturn()]); // ['x', 'y', 'Result: inner done']
28
29// Tree traversal
30function* traverse(node) {
31 yield node.value;
32 if (node.left) yield* traverse(node.left);
33 if (node.right) yield* traverse(node.right);
34}
35
36const tree = {
37 value: 1,
38 left: {
39 value: 2,
40 left: { value: 4 },
41 right: { value: 5 },
42 },
43 right: {
44 value: 3,
45 },
46};
47
48console.log([...traverse(tree)]); // [1, 2, 4, 5, 3]Generator Methods#
1// next() - resume execution
2// return() - end generator
3// throw() - throw error in generator
4
5function* controlledGenerator() {
6 try {
7 yield 1;
8 yield 2;
9 yield 3;
10 } finally {
11 console.log('Cleanup');
12 }
13}
14
15const gen1 = controlledGenerator();
16console.log(gen1.next()); // { value: 1, done: false }
17console.log(gen1.return('early')); // Cleanup, { value: 'early', done: true }
18
19// throw()
20function* errorHandlingGenerator() {
21 try {
22 yield 1;
23 yield 2;
24 } catch (error) {
25 console.log('Caught:', error.message);
26 yield 'recovered';
27 }
28}
29
30const gen2 = errorHandlingGenerator();
31console.log(gen2.next()); // { value: 1, done: false }
32gen2.throw(new Error('Oops')); // Caught: Oops
33console.log(gen2.next()); // { value: 'recovered', done: false }Async Generators#
1// Async generator function
2async function* asyncNumbers() {
3 for (let i = 1; i <= 3; i++) {
4 await new Promise(resolve => setTimeout(resolve, 100));
5 yield i;
6 }
7}
8
9// Using for-await-of
10async function main() {
11 for await (const num of asyncNumbers()) {
12 console.log(num); // 1, 2, 3 (with delays)
13 }
14}
15
16// Fetch pages
17async function* fetchPages(baseUrl) {
18 let page = 1;
19 let hasMore = true;
20
21 while (hasMore) {
22 const response = await fetch(`${baseUrl}?page=${page}`);
23 const data = await response.json();
24
25 yield data.items;
26
27 hasMore = data.hasNextPage;
28 page++;
29 }
30}
31
32// Process async stream
33async function processAllPages() {
34 for await (const items of fetchPages('/api/data')) {
35 for (const item of items) {
36 processItem(item);
37 }
38 }
39}Iterator Protocol#
1// Making objects iterable
2const range = {
3 from: 1,
4 to: 5,
5
6 *[Symbol.iterator]() {
7 for (let i = this.from; i <= this.to; i++) {
8 yield i;
9 }
10 },
11};
12
13console.log([...range]); // [1, 2, 3, 4, 5]
14
15// Iterable class
16class Range {
17 constructor(from, to) {
18 this.from = from;
19 this.to = to;
20 }
21
22 *[Symbol.iterator]() {
23 for (let i = this.from; i <= this.to; i++) {
24 yield i;
25 }
26 }
27}
28
29const r = new Range(1, 5);
30console.log([...r]); // [1, 2, 3, 4, 5]
31
32// Async iterable
33class AsyncRange {
34 constructor(from, to) {
35 this.from = from;
36 this.to = to;
37 }
38
39 async *[Symbol.asyncIterator]() {
40 for (let i = this.from; i <= this.to; i++) {
41 await new Promise(r => setTimeout(r, 100));
42 yield i;
43 }
44 }
45}Utility Functions#
1// Map over generator
2function* map(iterable, fn) {
3 for (const item of iterable) {
4 yield fn(item);
5 }
6}
7
8// Filter generator
9function* filter(iterable, predicate) {
10 for (const item of iterable) {
11 if (predicate(item)) {
12 yield item;
13 }
14 }
15}
16
17// Reduce generator
18function reduce(iterable, fn, initial) {
19 let accumulator = initial;
20 for (const item of iterable) {
21 accumulator = fn(accumulator, item);
22 }
23 return accumulator;
24}
25
26// Zip generators
27function* zip(...iterables) {
28 const iterators = iterables.map(i => i[Symbol.iterator]());
29
30 while (true) {
31 const results = iterators.map(i => i.next());
32
33 if (results.some(r => r.done)) {
34 return;
35 }
36
37 yield results.map(r => r.value);
38 }
39}
40
41// Usage
42const nums = [1, 2, 3, 4, 5];
43const doubled = [...map(nums, x => x * 2)]; // [2, 4, 6, 8, 10]
44const evens = [...filter(nums, x => x % 2 === 0)]; // [2, 4]
45const sum = reduce(nums, (a, b) => a + b, 0); // 15
46
47console.log([...zip([1, 2, 3], ['a', 'b', 'c'])]);
48// [[1, 'a'], [2, 'b'], [3, 'c']]Practical Patterns#
1// Pagination helper
2function* paginate(array, pageSize) {
3 for (let i = 0; i < array.length; i += pageSize) {
4 yield array.slice(i, i + pageSize);
5 }
6}
7
8const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
9for (const page of paginate(items, 3)) {
10 console.log(page);
11}
12// [1, 2, 3]
13// [4, 5, 6]
14// [7, 8, 9]
15// [10]
16
17// Chunked processing
18async function* processInChunks(items, chunkSize, processor) {
19 for (let i = 0; i < items.length; i += chunkSize) {
20 const chunk = items.slice(i, i + chunkSize);
21 const results = await Promise.all(chunk.map(processor));
22 yield* results;
23 }
24}
25
26// State machine
27function* trafficLight() {
28 while (true) {
29 yield 'green';
30 yield 'yellow';
31 yield 'red';
32 }
33}
34
35// ID generator
36function* idGenerator(prefix = '') {
37 let id = 0;
38 while (true) {
39 yield `${prefix}${++id}`;
40 }
41}
42
43const userId = idGenerator('user_');
44console.log(userId.next().value); // 'user_1'
45console.log(userId.next().value); // 'user_2'
46
47// Retry mechanism
48async function* retry(fn, maxAttempts, delay) {
49 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
50 try {
51 yield await fn();
52 return;
53 } catch (error) {
54 if (attempt === maxAttempts) {
55 throw error;
56 }
57 yield { attempt, error, retrying: true };
58 await new Promise(r => setTimeout(r, delay * attempt));
59 }
60 }
61}Coroutines#
1// Simple coroutine runner
2function run(generatorFn) {
3 const generator = generatorFn();
4
5 function handle(result) {
6 if (result.done) return Promise.resolve(result.value);
7
8 return Promise.resolve(result.value)
9 .then(res => handle(generator.next(res)))
10 .catch(err => handle(generator.throw(err)));
11 }
12
13 return handle(generator.next());
14}
15
16// Usage
17run(function* () {
18 const user = yield fetch('/api/user').then(r => r.json());
19 const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json());
20 console.log(posts);
21});
22
23// Cancellable operations
24function createCancellable(generatorFn) {
25 let cancelled = false;
26 const generator = generatorFn();
27
28 const promise = new Promise((resolve, reject) => {
29 function step(result) {
30 if (cancelled) {
31 reject(new Error('Cancelled'));
32 return;
33 }
34
35 if (result.done) {
36 resolve(result.value);
37 return;
38 }
39
40 Promise.resolve(result.value)
41 .then(value => step(generator.next(value)))
42 .catch(error => step(generator.throw(error)));
43 }
44
45 step(generator.next());
46 });
47
48 return {
49 promise,
50 cancel: () => { cancelled = true; },
51 };
52}Memory Efficiency#
1// Lazy evaluation - doesn't create full array
2function* range(start, end) {
3 for (let i = start; i <= end; i++) {
4 yield i;
5 }
6}
7
8// This is memory efficient
9for (const n of range(1, 1000000)) {
10 if (n > 10) break;
11 console.log(n);
12}
13
14// vs array which allocates all memory upfront
15// const arr = Array.from({ length: 1000000 }, (_, i) => i + 1);
16
17// Lazy file reading
18async function* readLines(file) {
19 const reader = file.getReader();
20 const decoder = new TextDecoder();
21 let buffer = '';
22
23 try {
24 while (true) {
25 const { done, value } = await reader.read();
26
27 if (done) {
28 if (buffer) yield buffer;
29 break;
30 }
31
32 buffer += decoder.decode(value, { stream: true });
33 const lines = buffer.split('\n');
34 buffer = lines.pop() || '';
35
36 for (const line of lines) {
37 yield line;
38 }
39 }
40 } finally {
41 reader.releaseLock();
42 }
43}Best Practices#
Usage:
✓ Use for lazy evaluation
✓ Use for infinite sequences
✓ Use for stateful iteration
✓ Use for async data streams
Patterns:
✓ Combine with yield* for delegation
✓ Use try/finally for cleanup
✓ Handle errors appropriately
✓ Use async generators for I/O
Performance:
✓ Prefer generators for large datasets
✓ Avoid unnecessary array allocations
✓ Use take/limit for infinite generators
✓ Clean up resources in finally
Avoid:
✗ Complex control flow in generators
✗ Forgetting to handle done state
✗ Ignoring cleanup on early exit
✗ Overusing generators for simple cases
Conclusion#
Generators provide powerful iteration control with lazy evaluation. Use them for infinite sequences, stateful iteration, async data streams, and memory-efficient processing. Combine with yield* for delegation and async/await for asynchronous operations.