Modern JavaScript provides powerful tools for handling sequential data, both synchronous and asynchronous. Understanding iterators, iterables, and their async counterparts is essential for writing efficient, readable code when dealing with streams of data.
What Are Iterators and Iterables?
Before diving into asynchronous iterators, let's establish the foundation with synchronous iterators and iterables.
Iterables
An iterable is any object that implements the iterable protocol by having a Symbol.iterator method. This method returns an iterator object. Common built-in iterables include arrays, strings, Maps, and Sets.
const myArray = [1, 2, 3];
// Arrays are iterables because they have a Symbol.iterator method
console.log(typeof myArray[Symbol.iterator]); // "function"
Iterators
An iterator is an object that implements the iterator protocol by having a next() method. This method returns an object with two properties:
-
value: the next value in the sequence -
done: a boolean indicating whether the iteration is complete
const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Creating Custom Iterables
You can create your own iterable objects:
const countdown = {
from: 5,
to: 1,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if (this.current >= this.last) {
return { value: this.current--, done: false };
} else {
return { done: true };
}
}
};
}
};
for (let num of countdown) {
console.log(num); // 5, 4, 3, 2, 1
}
Asynchronous Iterators and Iterables
Asynchronous iterators extend the iterator pattern to handle asynchronous data sources like API responses, file streams, or real-time data feeds.
Async Iterables
An async iterable implements the async iterable protocol by having a Symbol.asyncIterator method that returns an async iterator.
Async Iterators
An async iterator has a next() method that returns a Promise that resolves to an object with value and done properties.
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
next() {
if (i < 3) {
return Promise.resolve({ value: i++, done: false });
}
return Promise.resolve({ done: true });
}
};
}
};
// Using for-await-of loop
(async () => {
for await (const num of asyncIterable) {
console.log(num); // 0, 1, 2
}
})();
Key Differences
| Feature | Synchronous | Asynchronous |
|---|---|---|
| Protocol Symbol | Symbol.iterator |
Symbol.asyncIterator |
| next() returns | { value, done } |
Promise<{ value, done }> |
| Consumption |
for...of loop |
for await...of loop |
| Use Case | Immediate data | Data that arrives over time |
| Blocking | Synchronous, blocks execution | Non-blocking, uses promises |
Practical Example: Async Data Fetching
Here's a real-world example of fetching paginated data:
class PaginatedAPI {
constructor(baseUrl, pageSize = 10) {
this.baseUrl = baseUrl;
this.pageSize = pageSize;
}
async *[Symbol.asyncIterator]() {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`${this.baseUrl}?page=${page}&size=${this.pageSize}`
);
const data = await response.json();
for (const item of data.items) {
yield item;
}
hasMore = data.hasNext;
page++;
}
}
}
// Usage
(async () => {
const api = new PaginatedAPI('https://api.example.com/data');
for await (const item of api) {
console.log(item);
// Process each item as it arrives
}
})();
Async Generator Functions
Just as generator functions simplify creating synchronous iterators, async generator functions make creating async iterators easier:
async function* fetchUserData(userIds) {
for (const id of userIds) {
const response = await fetch(`/api/users/${id}`);
const userData = await response.json();
yield userData;
}
}
// Usage
(async () => {
const users = fetchUserData([1, 2, 3, 4, 5]);
for await (const user of users) {
console.log(`Processing ${user.name}`);
}
})();
Real-World Use Cases
1. Reading Large Files in Chunks
async function* readFileInChunks(filePath, chunkSize = 1024) {
const fileHandle = await fs.open(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
while (true) {
const { bytesRead } = await fileHandle.read(buffer, 0, chunkSize);
if (bytesRead === 0) break;
yield buffer.slice(0, bytesRead);
}
await fileHandle.close();
}
2. Processing Streaming Data
async function* processLogStream(logUrl) {
const response = await fetch(logUrl);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim()) {
yield JSON.parse(line);
}
}
}
}
3. Rate-Limited API Calls
async function* rateLimitedFetch(urls, requestsPerSecond) {
const delay = 1000 / requestsPerSecond;
for (const url of urls) {
const startTime = Date.now();
const response = await fetch(url);
const data = await response.json();
yield data;
const elapsed = Date.now() - startTime;
if (elapsed < delay) {
await new Promise(resolve => setTimeout(resolve, delay - elapsed));
}
}
}
Best Practices
- Error Handling: Always wrap async iteration in try-catch blocks to handle failures gracefully.
(async () => {
try {
for await (const item of asyncIterable) {
// Process item
}
} catch (error) {
console.error('Iteration failed:', error);
}
})();
Cleanup: Implement cleanup logic for resources using
return()andthrow()methods in your iterators.Memory Management: Be mindful of memory when working with large data streams. Process items as they arrive rather than accumulating them.
Cancellation: Consider implementing cancellation mechanisms for long-running async iterations.
Browser and Node.js Support
Async iterators are supported in:
- Node.js 10+
- Chrome 63+
- Firefox 57+
- Safari 12+
- Edge 79+
Conclusion
Asynchronous iterators and iterables provide a elegant, standardized way to work with asynchronous data sequences in JavaScript. They bridge the gap between synchronous iteration patterns and promise-based asynchronous code, making it easier to write readable, maintainable code for streaming data, paginated APIs, and other async operations.
By understanding both synchronous and asynchronous iteration protocols, you can choose the right tool for your data processing needs and write more expressive JavaScript code.
Top comments (0)