Learning Objectives

  • Understand async generator syntax
  • Master for-await-of loops
  • Build async data streams
  • Handle async iteration patterns

What are Async Generators?

Async generators combine generators with async/await, allowing you to yield Promises and handle asynchronous data streams.

async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

// Consume with for-await-of
(async () => {
  for await (const value of asyncGenerator()) {
    console.log(value); // 1, 2, 3
  }
})();

Async Generator Syntax

// Function declaration
async function* myAsyncGen() {
  yield 1;
  yield await fetchData();
}

// Function expression
const myAsyncGen = async function* () {
  yield 1;
  yield await fetchData();
};

// Arrow function (not supported)
// const myAsyncGen = async* () => {}; // ❌ Syntax error

// Object method
const obj = {
  async *myAsyncGen() {
    yield 1;
    yield await fetchData();
  }
};

// Class method
class MyClass {
  async *myAsyncGen() {
    yield 1;
    yield await fetchData();
  }
}

for-await-of Loop

async function* numbers() {
  for (let i = 1; i <= 3; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

(async () => {
  for await (const num of numbers()) {
    console.log(num); // 1 (after 1s), 2 (after 2s), 3 (after 3s)
  }
})();

Real-World: Paginated API

async function* fetchPages(url) {
  let page = 1;
  let hasMore = true;
  
  while (hasMore) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    
    yield data.items;
    
    hasMore = data.hasMore;
    page++;
  }
}

// Usage
(async () => {
  for await (const items of fetchPages('/api/users')) {
    console.log('Page:', items);
    // Process each page as it arrives
  }
})();

Real-World: File Reader

async function* readFileInChunks(file, chunkSize = 1024) {
  let offset = 0;
  
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const text = await chunk.text();
    yield text;
    offset += chunkSize;
  }
}

// Usage
(async () => {
  const file = new File(['Hello World...'], 'test.txt');
  
  for await (const chunk of readFileInChunks(file, 5)) {
    console.log('Chunk:', chunk);
  }
})();

Real-World: Event Stream

async function* eventStream(eventTarget, eventName) {
  const queue = [];
  let resolve;
  
  const listener = event => {
    if (resolve) {
      resolve(event);
      resolve = null;
    } else {
      queue.push(event);
    }
  };
  
  eventTarget.addEventListener(eventName, listener);
  
  try {
    while (true) {
      if (queue.length > 0) {
        yield queue.shift();
      } else {
        yield await new Promise(r => resolve = r);
      }
    }
  } finally {
    eventTarget.removeEventListener(eventName, listener);
  }
}

// Usage
(async () => {
  const button = document.querySelector('#myButton');
  
  for await (const event of eventStream(button, 'click')) {
    console.log('Button clicked!', event);
    if (event.detail === 3) break; // Stop after 3 clicks
  }
})();

Async Generator Pipeline

async function* fetchUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();
  
  for (const user of users) {
    yield user;
  }
}

async function* enrichUsers(userStream) {
  for await (const user of userStream) {
    const details = await fetch(`/api/users/${user.id}/details`);
    const enriched = await details.json();
    yield { ...user, ...enriched };
  }
}

async function* filterActiveUsers(userStream) {
  for await (const user of userStream) {
    if (user.active) {
      yield user;
    }
  }
}

// Compose pipeline
(async () => {
  const users = fetchUsers();
  const enriched = enrichUsers(users);
  const active = filterActiveUsers(enriched);
  
  for await (const user of active) {
    console.log('Active user:', user);
  }
})();

Real-World: Database Cursor

async function* queryDatabase(query) {
  const cursor = await db.collection('users').find(query);
  
  while (await cursor.hasNext()) {
    yield await cursor.next();
  }
  
  await cursor.close();
}

// Usage
(async () => {
  for await (const user of queryDatabase({ age: { $gt: 18 } })) {
    console.log(user);
  }
})();

Error Handling

async function* withErrorHandling() {
  try {
    yield await fetch('/api/data1');
    yield await fetch('/api/data2');
    yield await fetch('/api/data3');
  } catch (error) {
    console.error('Error:', error);
    yield { error: true, message: error.message };
  } finally {
    console.log('Cleanup');
  }
}

(async () => {
  for await (const data of withErrorHandling()) {
    console.log(data);
  }
})();

Combining Async and Sync Generators

function* syncGen() {
  yield 1;
  yield 2;
  yield 3;
}

async function* asyncWrapper(syncGenerator) {
  for (const value of syncGenerator) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield value;
  }
}

(async () => {
  for await (const value of asyncWrapper(syncGen())) {
    console.log(value); // 1, 2, 3 (with delays)
  }
})();

Real-World: WebSocket Stream

async function* websocketStream(url) {
  const ws = new WebSocket(url);
  const queue = [];
  let resolve;
  
  ws.onmessage = event => {
    if (resolve) {
      resolve(event.data);
      resolve = null;
    } else {
      queue.push(event.data);
    }
  };
  
  await new Promise(r => ws.onopen = r);
  
  try {
    while (ws.readyState === WebSocket.OPEN) {
      if (queue.length > 0) {
        yield queue.shift();
      } else {
        yield await new Promise(r => resolve = r);
      }
    }
  } finally {
    ws.close();
  }
}

// Usage
(async () => {
  for await (const message of websocketStream('ws://localhost:8080')) {
    console.log('Received:', message);
  }
})();

Key Takeaways

  • ✅ Use async function* for async generators
  • ✅ Consume with for-await-of loops
  • ✅ Perfect for async data streams
  • ✅ Great for paginated APIs
  • ✅ Handle real-time data (WebSockets, events)
  • ✅ Combine with regular generators for pipelines

Next Steps

Now let's explore practical use cases for generators in real applications!