Skip to main content
โšก Calmops

Async Generators and Iteration in JavaScript

Async Generators and Iteration in JavaScript

Async generators combine generators with async/await for elegant async sequence handling. This article covers async generators, async iteration, and practical applications.

Introduction

Async generators enable:

  • Elegant async sequence handling
  • Streaming data processing
  • Async iteration patterns
  • Memory-efficient async operations
  • Simplified async workflows

Understanding async generators helps you:

  • Handle async sequences elegantly
  • Process streaming data
  • Build responsive applications
  • Manage complex async workflows

Async Iterables

Async Iterable Protocol

// An object is async iterable if it has Symbol.asyncIterator
// that returns an async iterator

const asyncIterable = {
  [Symbol.asyncIterator]() {
    return {
      async next() {
        // Return Promise<{ value, done }>
      }
    };
  }
};

// Can be used in for await...of loops
for await (const value of asyncIterable) {
  console.log(value);
}

Creating Custom Async Iterables

// โœ… Good: Create a custom async iterable
class AsyncRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.asyncIterator]() {
    let current = this.start;
    const end = this.end;

    return {
      async next() {
        // Simulate async operation
        await new Promise(resolve => setTimeout(resolve, 100));
        
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

// Usage
async function main() {
  const range = new AsyncRange(1, 5);
  for await (const num of range) {
    console.log(num); // 1, 2, 3, 4, 5 (with delays)
  }
}

main();
// Practical example: Async iterable for API pagination
class PaginatedAPI {
  constructor(baseUrl, pageSize = 10) {
    this.baseUrl = baseUrl;
    this.pageSize = pageSize;
  }

  [Symbol.asyncIterator]() {
    let page = 1;
    const baseUrl = this.baseUrl;
    const pageSize = this.pageSize;

    return {
      async next() {
        const response = await fetch(
          `${baseUrl}?page=${page}&limit=${pageSize}`
        );
        const data = await response.json();

        if (data.items.length === 0) {
          return { done: true };
        }

        page++;
        return { value: data.items, done: false };
      }
    };
  }
}

// Usage
async function fetchAllItems() {
  const api = new PaginatedAPI('https://api.example.com/items');
  
  for await (const items of api) {
    console.log('Got items:', items);
  }
}

Async Generators

Basic Async Generators

// Async generator function: uses async function* and yield
async function* simpleAsyncGenerator() {
  yield 1;
  await new Promise(resolve => setTimeout(resolve, 100));
  yield 2;
  await new Promise(resolve => setTimeout(resolve, 100));
  yield 3;
}

// Usage
async function main() {
  for await (const value of simpleAsyncGenerator()) {
    console.log(value); // 1, 2, 3 (with delays)
  }
}

main();
// Async generators are async iterables
async function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

// Can be used in for await...of
async function main() {
  for await (const num of countTo(5)) {
    console.log(num); // 1, 2, 3, 4, 5
  }
}

main();

Async Generator with Error Handling

// โœ… Good: Handle errors in async generators
async function* fetchData(urls) {
  for (const url of urls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      yield data;
    } catch (error) {
      console.error(`Failed to fetch ${url}:`, error);
      yield null; // Yield null on error
    }
  }
}

// Usage
async function main() {
  const urls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3'
  ];

  for await (const data of fetchData(urls)) {
    if (data) {
      console.log('Data:', data);
    }
  }
}

main();

Practical Async Generator Patterns

Reading Streams

// โœ… Good: Read stream data with async generator
async function* readStream(stream) {
  const reader = stream.getReader();
  
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

// Usage
async function processStream(url) {
  const response = await fetch(url);
  
  for await (const chunk of readStream(response.body)) {
    console.log('Chunk:', chunk);
  }
}

Polling with Async Generators

// โœ… Good: Poll API with async generator
async function* pollAPI(url, interval = 1000) {
  while (true) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      yield data;
    } catch (error) {
      console.error('Poll error:', error);
    }
    
    await new Promise(resolve => setTimeout(resolve, interval));
  }
}

// Usage
async function monitorAPI() {
  for await (const data of pollAPI('https://api.example.com/status')) {
    console.log('Status:', data);
    
    // Stop after 10 iterations
    if (data.count > 10) break;
  }
}

monitorAPI();

Batching Async Operations

// โœ… Good: Batch async operations
async function* batch(iterable, batchSize) {
  let batch = [];
  
  for await (const item of iterable) {
    batch.push(item);
    
    if (batch.length === batchSize) {
      yield batch;
      batch = [];
    }
  }
  
  if (batch.length > 0) {
    yield batch;
  }
}

// Usage
async function processBatches() {
  async function* items() {
    for (let i = 1; i <= 10; i++) {
      yield i;
    }
  }
  
  for await (const batchItems of batch(items(), 3)) {
    console.log('Batch:', batchItems);
    // Process batch
  }
}

processBatches();

Transforming Async Sequences

// โœ… Good: Transform async sequences
async function* map(iterable, fn) {
  for await (const value of iterable) {
    yield await fn(value);
  }
}

async function* filter(iterable, predicate) {
  for await (const value of iterable) {
    if (await predicate(value)) {
      yield value;
    }
  }
}

// Usage
async function* numbers() {
  for (let i = 1; i <= 10; i++) {
    yield i;
  }
}

async function main() {
  const result = filter(
    map(numbers(), async x => x * 2),
    async x => x % 3 === 0
  );
  
  for await (const value of result) {
    console.log(value); // 6, 12, 18
  }
}

main();

Delegating to Other Async Generators

// โœ… Good: Use yield* with async generators
async function* generator1() {
  yield 1;
  yield 2;
}

async function* generator2() {
  yield 3;
  yield 4;
}

async function* combined() {
  yield* generator1();
  yield* generator2();
}

// Usage
async function main() {
  for await (const value of combined()) {
    console.log(value); // 1, 2, 3, 4
  }
}

main();

Advanced Patterns

Async Generator with Backpressure

// โœ… Good: Handle backpressure
async function* producer() {
  for (let i = 0; i < 100; i++) {
    console.log('Producing:', i);
    yield i;
  }
}

async function consumer() {
  let count = 0;
  
  for await (const value of producer()) {
    console.log('Consuming:', value);
    count++;
    
    // Simulate slow processing
    await new Promise(resolve => setTimeout(resolve, 100));
    
    if (count >= 10) break;
  }
}

consumer();

Async Generator with Cleanup

// โœ… Good: Cleanup resources
async function* withCleanup() {
  const resource = await acquireResource();
  
  try {
    for (let i = 0; i < 5; i++) {
      yield i;
    }
  } finally {
    await resource.cleanup();
  }
}

async function acquireResource() {
  return {
    cleanup: async () => {
      console.log('Cleaning up');
    }
  };
}

// Usage
async function main() {
  for await (const value of withCleanup()) {
    console.log(value);
  }
}

main();

Async Generator with Timeout

// โœ… Good: Add timeout to async generator
async function* withTimeout(iterable, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), timeoutMs)
  );
  
  for await (const value of iterable) {
    try {
      yield await Promise.race([
        Promise.resolve(value),
        timeoutPromise
      ]);
    } catch (error) {
      console.error('Timeout:', error);
      break;
    }
  }
}

// Usage
async function* slowGenerator() {
  for (let i = 0; i < 10; i++) {
    await new Promise(resolve => setTimeout(resolve, 500));
    yield i;
  }
}

async function main() {
  for await (const value of withTimeout(slowGenerator(), 2000)) {
    console.log(value);
  }
}

main();

Comparison: Generators vs Async Generators

// Regular Generator
function* regularGen() {
  yield 1;
  yield 2;
}

// Async Generator
async function* asyncGen() {
  yield 1;
  yield 2;
}

// Usage difference
for (const value of regularGen()) {
  console.log(value); // Synchronous
}

for await (const value of asyncGen()) {
  console.log(value); // Asynchronous
}

Best Practices

  1. Use async generators for async sequences:

    // โœ… Good
    async function* fetchPages(url) {
      for await (const page of paginate(url)) {
        yield page;
      }
    }
    
  2. Handle errors properly:

    // โœ… Good
    async function* safe() {
      try {
        yield await operation();
      } catch (error) {
        console.error(error);
      }
    }
    
  3. Clean up resources:

    // โœ… Good
    async function* withCleanup() {
      const resource = await acquire();
      try {
        yield resource;
      } finally {
        await resource.cleanup();
      }
    }
    
  4. Use for await…of:

    // โœ… Good
    for await (const value of asyncGen()) {
      console.log(value);
    }
    

Common Mistakes

  1. Forgetting async in function:*

    // โŒ Bad - not async
    function* gen() {
      yield await promise;
    }
    
    // โœ… Good
    async function* gen() {
      yield await promise;
    }
    
  2. Using for…of instead of for await…of:

    // โŒ Bad
    for (const value of asyncGen()) {
      console.log(value);
    }
    
    // โœ… Good
    for await (const value of asyncGen()) {
      console.log(value);
    }
    
  3. Not handling errors:

    // โŒ Bad
    async function* gen() {
      yield await fetch(url);
    }
    
    // โœ… Good
    async function* gen() {
      try {
        yield await fetch(url);
      } catch (error) {
        console.error(error);
      }
    }
    

Summary

Async generators are powerful for async sequences. Key takeaways:

  • Async iterables have Symbol.asyncIterator
  • Async generators use async function* and yield
  • Use for await…of to iterate
  • Perfect for streams and async sequences
  • Handle errors and cleanup properly
  • Combine with other async patterns

Next Steps

Comments