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
-
Use async generators for async sequences:
// โ Good async function* fetchPages(url) { for await (const page of paginate(url)) { yield page; } } -
Handle errors properly:
// โ Good async function* safe() { try { yield await operation(); } catch (error) { console.error(error); } } -
Clean up resources:
// โ Good async function* withCleanup() { const resource = await acquire(); try { yield resource; } finally { await resource.cleanup(); } } -
Use for await…of:
// โ Good for await (const value of asyncGen()) { console.log(value); }
Common Mistakes
-
Forgetting async in function:*
// โ Bad - not async function* gen() { yield await promise; } // โ Good async function* gen() { yield await promise; } -
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); } -
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
Related Resources
Next Steps
- Learn about Concurrency Patterns in JavaScript
- Explore Rate Limiting and Throttling
- Study Generators and Iterators
- Practice with streaming data
- Build async pipelines
Comments