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 };
},
};
},
};
};
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);
}
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);
}
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 };
},
};
},
};
};
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 };
},
};
},
};
}
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);
}
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)
Great thank you :-)
You could be interested by JSCoroutines
Iβve never seen it before! But Iβll take a look at it in the following weeks π
Thanks for the advice π
My write up on how JSCoroutines works:
60fps Javascript while you stringify, parse, process, compress and filter 100Mbs of data
Mike Talbot β γ» May 25 '20
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.
Great write up!
I wonder about use cases where async generators shine. Happy to hear about real world examples.
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 π
No doubts :) Thank you!
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.
Glad the article was appreciated π
amazing explanation ππ
Very good explanation
Thanks Patric! Iβm glad youβve appreciated the article π