Mastering JavaScript Async Iterators: Unlocking Asynchronous Magic
Luca Del Puppo

Luca Del Puppo @puppo

About: @Microsoft MVP, @Google Developer Expert, @GitKraken Ambassador, Senior Software Developer and JavaScript enthusiastic Youtube Channel https://youtube.com/@Puppo_92

Location:
Povegliano, TV
Joined:
Dec 1, 2020

Mastering JavaScript Async Iterators: Unlocking Asynchronous Magic

Publish Date: Sep 8 '23
28 11

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.

Comments 11 total

  • artydev
    artydevSep 8, 2023

    Great thank you :-)

    You could be interested by JSCoroutines

  • Patric
    PatricSep 8, 2023

    Very good explanation

    • Luca Del Puppo
      Luca Del PuppoSep 8, 2023

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

  • Sascha Picard
    Sascha Picard Sep 9, 2023

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

    • Luca Del Puppo
      Luca Del PuppoSep 9, 2023

      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 🙂

  • Hasan Elsherbiny
    Hasan ElsherbinySep 10, 2023

    amazing explanation 👏👏

  • Shubhankar Valimbe
    Shubhankar ValimbeSep 14, 2023

    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.

Add comment