DEV Community

Cover image for Mastering JavaScript Async Iterators: Unlocking Asynchronous Magic
Luca Del Puppo for This is Learning

Posted on • Originally published at blog.delpuppo.net on

Mastering JavaScript Async Iterators: Unlocking Asynchronous Magic

In the ever-evolving landscape of JavaScript, staying up-to-date with its latest features is crucial for writing efficient and modern code. One such feature that has garnered significant attention is the Async Iterator. While iterators have long been an integral part of JavaScript for sequential data processing, the introduction of asynchronous programming patterns brought about the need for asynchronous iteration.

Imagine effortlessly traversing through data streams that might involve fetching data from APIs, reading from files, or any other asynchronous data source. This is precisely where Async Iterators shine, providing a seamless and elegant solution to handle such scenarios. In this blog post, we'll delve into the world of JavaScript Async Iterators, exploring their fundamentals, understanding their benefits, and uncovering how they can be a game-changer in writing robust asynchronous code. Whether you're a seasoned developer looking to expand your skill set or a newcomer curious about advanced JavaScript techniques, this blog post is for you. We will unravel the power of Async Iterators and take your asynchronous programming skills to new heights.

Before we jump into the code, let's understand what async iterators are. In JavaScript, iterators are objects that allow us to loop over collections. Async iterators take this concept a step further by allowing us to handle asynchronous operations, like fetching data from APIs or reading from streams.

Creating an async iterable is simple. We use the Symbol.asyncIterator to define the async iterator method inside an object. This method will return an object with the next method that resolves a promise containing the next value in the asynchronous sequence.

Let's take a look at an example.

const getUsers = (ids: number[]): AsyncIterable<User> => {
  return {
    [Symbol.asyncIterator]() {
      let i = 0;
      return {
        async next() {
          console.log("getUsers next");
          if (i === ids.length) {
            return { done: true, value: null };
          }
          const data = await fetch(
            `https://reqres.in/api/users/${ids[i++]}`
          ).then(res => res.json());
          return { done: false, value: data };
        },
      };
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Imagine you have a list of IDs and want to read the user data only if needed. Using AsyncIterators, you can create a function that handles the API and returns the result of every request on every iteration, making the code more transparent.

To consume the values of an async iterable, we use the for-await-of loop. This loop works just like the regular for-of loop, but it's designed specifically for asynchronous iterables.

for await (const user of getUsers([1, 2, 3, 4, 5])) {
  console.log(user);
}
Enter fullscreen mode Exit fullscreen mode

Error handling is crucial when dealing with asynchronous operations. Async iterators allow us to handle errors using try-catch blocks around the for-await-of loop.

try {
    for await (const user of getUsers([1, 2, 3, 4, 5])) {
      console.log(user);
    }
  } catch (err) {
    console.error(err);
  }
Enter fullscreen mode Exit fullscreen mode

AsyncIterator runs code only if needed, so until you don't call the next method, nothing happens, like for Iterators.

The return method exists also for AsyncIterators. This method is used in case the code doesn't complete all the iterations. Imagine the loop calls a break or a return; in this case, JavaScript under the hood calls the return method for us. In this method, we can handle whatever we need. We may need to reset something or check the current value of the iterator.

const getUsers = (ids: number[]): AsyncIterable<User> => {
  return {
    [Symbol.asyncIterator]() {
      let i = 0;
      return {
        ...
        async return() {
          console.log("getUsers return");
          return { done: true, value: null };
        },
      };
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Async Iterators are powerful like Iterators, and we can create functions that accept an AsyncIterator and manipulate it to return another Async Iterator. For instance, we can create a map function that accepts an Async Iterator and returns another with a callback specified by the user.

function map<T, U>(iter: AsyncIterable<T>, fn: (v: T) => U): AsyncIterable<U> {
  return {
    [Symbol.asyncIterator]() {
      const iterator = iter[Symbol.asyncIterator]();
      return {
        async next() {
          console.log("map next");
          const { done, value } = await iterator.next();
          if (done) return { done, value: null };
          return { done, value: fn(value) };
        },
        async return() {
          console.log("map return");
          if (iterator?.return) await iterator?.return();
          return { done: true, value: null };
        },
      };
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

These functions have all the benefits said before. Javascript does nothing until the codebase doesn't ask for the next function; the same is true for the return method, and now you can compose the getUsers with the map to build a new Async Iterator.

const iterator = map(getUsers([1, 2, 3, 4, 5]), user => user.data.id)
for await (const num of iterator) {
  if (num === 3) break;
  console.log(num);
}
Enter fullscreen mode Exit fullscreen mode

And there you have it a deep dive into the world of asynchronous iterators in JavaScript. They provide an elegant solution to working with asynchronous data streams, making your code more organized and efficient. Experiment with async iterators in your projects, and you'll be amazed at how they simplify complex asynchronous workflows.

I also created a video on my Youtube channel, that you can find below.

If you found this content helpful, like and share it. And if you have any questions, feedback, or doubts, let me know in the comments 😀

Thanks for reading! And Happy coding! 👩💻 👨💻

N.B. you can find the code of this post here.

Top comments (11)

Collapse
 
artydev profile image
artydev

Great thank you :-)

You could be interested by JSCoroutines

Collapse
 
puppo profile image
Luca Del Puppo

I’ve never seen it before! But I’ll take a look at it in the following weeks 🙂
Thanks for the advice 🙂

Collapse
 
miketalbot profile image
Mike Talbot ⭐

My write up on how JSCoroutines works:

It uses generators a lot, but mostly to create imperative animations or smooth reactions by splitting operations over multiple frames to avoid jank (like some of the new React stuff).

I use a lot of AsyncIterators in my code, mostly around streaming the reaction to periodic events.

Collapse
 
spic profile image
Sascha Picard • Edited

Great write up!
I wonder about use cases where async generators shine. Happy to hear about real world examples.

Collapse
 
puppo profile image
Luca Del Puppo

Hey Sascha,
One of the common use cases is when you stream in Nodejs and you need to transform data. In NodeJs, streams already implement the Async Iterators interface too.
Another great example is infinitive loading in a Frontend Application, for instance. Let me know if I answered to your doubts 🙂

Collapse
 
spic profile image
Sascha Picard

No doubts :) Thank you!

Collapse
 
shubhankarval profile image
Shubhankar Valimbe

I really enjoyed this article on async iterators! I'm still learning about them, but I can see how they can be a powerful tool for writing asynchronous code. I'm definitely going to try to use them more in my own projects.

Collapse
 
puppo profile image
Luca Del Puppo

Glad the article was appreciated 🙌

Collapse
 
hasanelsherbiny profile image
Hasan Elsherbiny

amazing explanation 👏👏

Collapse
 
patric12 profile image
Patric

Very good explanation

Collapse
 
puppo profile image
Luca Del Puppo

Thanks Patric! I’m glad you’ve appreciated the article 🙌