JavaScript's iterator protocol has evolved from synchronous to asynchronous, becoming a core capability for handling streaming data. In 2026, the combination of AsyncIterator and Generator is more mature, providing elegant solutions for complex data processing scenarios.
Iterator Protocol Basic Review
The core of synchronous iterators is Symbol.iterator:
interface Iterator<T> {
next(): { value: T; done: boolean }
return?(value?: T): { value: T; done: boolean }
throw?(e?: any): { value: T; done: boolean }
}
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>
}
Async iterators add the async dimension:
interface AsyncIterator<T> {
next(): Promise<{ value: T; done: boolean }>
return?(value?: T): Promise<{ value: T; done: boolean }>
throw?(e?: any): Promise<{ value: T; done: boolean }>
}
interface AsyncIterable<T> {
[Symbol.asyncIterator](): AsyncIterator<T>
}
Generator Advanced Usage
Bidirectional Generator
Generators can not only produce values but also receive them:
function* bidirectional() {
const commands: string[] = []
while (true) {
const command = yield // Receive external input
switch (command) {
case 'start':
commands.push('started')
yield 'started' // Produce result
break
case 'stop':
commands.push('stopped')
yield 'stopped'
break
case 'status':
yield `History: ${commands.join(', ')}`
break
}
}
}
const gen = bidirectional()
gen.next() // Start iterator
console.log(gen.next('start').value) // 'started'
console.log(gen.next('stop').value) // 'stopped'
console.log(gen.next('status').value) // 'History: started, stopped'
Buffered Generator
function* bufferedGenerator<T>(
source: Iterable<T>,
bufferSize: number
): Generator<T[]> {
let buffer: T[] = []
for (const item of source) {
buffer.push(item)
if (buffer.length >= bufferSize) {
yield [...buffer]
buffer = []
}
}
// Handle remaining elements
if (buffer.length > 0) {
yield buffer
}
}
// Usage example
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const batches = bufferedGenerator(data, 3)
for (const batch of batches) {
console.log(batch) // [1,2,3], [4,5,6], [7,8,9], [10]
}
AsyncIterator Practical Applications
Streaming Data Processing
async function* readLines(filePath: string): AsyncGenerator<string> {
const { createReadStream } = await import('node:fs')
const stream = createReadStream(filePath, { encoding: 'utf8' })
let buffer = ''
for await (const chunk of stream) {
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.trim()) {
yield line
}
}
}
if (buffer.trim()) {
yield buffer
}
}
// Usage example
for await (const line of readLines('data.jsonl')) {
const data = JSON.parse(line)
console.log(data)
}
Pagination API Handling
async function* paginateApi<T>(
fetchFn: (page: number) => Promise<{ data: T[]; hasMore: boolean }>
): AsyncGenerator<T> {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetchFn(page)
for (const item of response.data) {
yield item
}
hasMore = response.hasMore
page++
}
}
// Usage: automatically handle pagination
async function getAllUsers() {
const users: User[] = []
for await (const user of paginateApi(async (page) => {
const response = await fetch(`/api/users?page=${page}`)
const data = await response.json()
return {
data: data.users,
hasMore: data.page < data.totalPages
}
})) {
users.push(user)
}
return users
}
Composition Patterns
Pipeline Pattern
function pipeline<T>(
...generators: Array<(input: any) => Generator<any>>
): Generator<any> {
let input: any = undefined
for (const generatorFn of generators) {
const gen = generatorFn(input)
let result = gen.next()
while (!result.done) {
yield result.value
result = gen.next()
}
input = result.value
}
}
// Usage example
function* readSource(): Generator<string> {
yield ' Hello World '
yield ' Foo Bar '
}
function* trim(items: Iterable<string>): Generator<string> {
for (const item of items) {
yield item.trim()
}
}
function* toUpperCase(items: Iterable<string>): Generator<string> {
for (const item of items) {
yield item.toUpperCase()
}
}
const result = [...pipeline(readSource, trim, toUpperCase)]
// ['HELLO WORLD', 'FOO BAR']
Error Handling
Resilient AsyncIterator
async function* resilientIterator<T>(
source: AsyncIterable<T>,
maxRetries: number = 3
): AsyncGenerator<T> {
let retryCount = 0
const iterator = source[Symbol.asyncIterator]()
while (true) {
try {
const result = await iterator.next()
if (result.done) {
break
}
retryCount = 0 // Reset retry count on success
yield result.value
} catch (error) {
retryCount++
if (retryCount > maxRetries) {
throw new Error(`Max retries exceeded: ${error}`)
}
console.warn(`Retrying ${retryCount}/${maxRetries}: ${error}`)
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount))
}
}
}
Performance Optimization
Batch Processing
async function* batchProcessor<T>(
source: AsyncIterable<T>,
batchSize: number,
processor: (batch: T[]) => Promise<any>
): AsyncGenerator<any> {
let batch: T[] = []
for await (const item of source) {
batch.push(item)
if (batch.length >= batchSize) {
yield await processor([...batch])
batch = []
}
}
if (batch.length > 0) {
yield await processor(batch)
}
}
Summary
AsyncIterator and Generator are JavaScript's core capabilities for handling streaming data. From simple pagination handling to complex concurrency control, these patterns have wide applications in real projects. The key to mastering these advanced patterns is understanding "lazy evaluation" and "backpressure control"—letting data be produced on demand rather than loading all data at once.
