DEV Community

sizan mahmud0
sizan mahmud0

Posted on

Exploring Asynchronous Iterators and Iterables in JavaScript

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"
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  }
})();
Enter fullscreen mode Exit fullscreen mode

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
  }
})();
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
})();
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. 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);
  }
})();
Enter fullscreen mode Exit fullscreen mode
  1. Cleanup: Implement cleanup logic for resources using return() and throw() methods in your iterators.

  2. Memory Management: Be mindful of memory when working with large data streams. Process items as they arrive rather than accumulating them.

  3. 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)