ES2018 - Asynchronous Iteration
One of the most interesting features in ES2018, which is still pretty new, is Asynchronous Iterations. In order to understand Asynchronous Iterations, we need to dive a little into the amazing world of iterators. And I promise you, it’s going to be a lot of fun.
How do we perform iterations? Well, every JavaScript programmer knows that—you take a JavaScript array and loop over it.
const a = [1, 2, 3];
for(let i in a) {
console.log(i); // 0 1 2
}
But what’s going on behind the scenes? Well, behind the scenes, the array has an iteration function that runs each time we run a for if or any other iteration function, like for each for example. So really, the code above is in essence this:
const a = [1, 2, 3];
const iteratorOfA = a[Symbol.iterator]();
let result;
result = iteratorOfA.next();
console.log(result); // { value: 1, done: false }
result = iteratorOfA.next();
console.log(result); // { value: 2, done: false }
result = iteratorOfA.next();
console.log(result); // { value: 3, done: false }
result = iteratorOfA.next();
console.log(result); // { value: undefined, done: true}
If feel pretty confident saying most readers of this article were unaware of this, that underneath of that loveable little loop of ours there is essentially an iteration function. But wait, there’s more. Are you ready to be really shocked? It’s also possible to change this function or put it where it doesn’t usually exist. So let’s take a normal object as an example:
const myObj = {
key1: 'val1',
key2: 'val2',
}
for(let i of myObj) {
console.log(i);
}
What do we get back? An error saying: myObj is not iterable. Why? Because the for of function works solely on arrays! I even wrote about this once. But we can put an iteration function in an object and thus make for of work with it. How you ask? Using Symbol.iterator, which takes a function (notice I didn’t use an arrow function here) that does the sorting. That’s it!
const myObj = {
key1: 'val1',
key2: 'val2',
[Symbol.iterator]: function() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
if (i == keys.length) return {value: null, done: true};
return {
value: [keys[i], this[keys[i++]]],
done: false
};
}
}
}
}
for(let i of myObj) {
console.log(i); // ["key1", "val1"], ["key2", "val2"]
}
Want to mess around with it? It’s really quite nice. Just make sure you always use i because if you don’t you’ll wind up in an infinite loop. Long story short, we just need to make sure we always return done:true at some point—that’s it really.
You can really go wild here. For example, you could decide that in each iteration it will be written: “Ran the King.”
const myObj = {
key1: 'val1',
key2: 'val2',
[Symbol.iterator]: function() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
if (i == keys.length) return {value: null, done: true};
const result = {
value: this[keys[i]] + ' Ran The King',
done: false
};
i++;
return result;
}
}
}
}
for(let i of myObj) {
console.log(i); // val1 Ran The King, val2 Ran The King
}
OK ok, maybe that was a bit of a brain overload, but there really isn’t anything new here so far. So what is the new thing? We can do asynchronous iterations that return functions! You’re excited, I know. Me too! It’s incredibly useful for a million things and you can use it on asynchronous functions that are marked as async.
This would be a good time to go back over promises and async-await.
Sound complicated? It’s really not. It just like a regular iteration, just that instead of returning a value, we return promise!
async function fetchAsync (i) {
let response = await fetch('https://icanhazdadjoke.com/', {headers: {'Accept': 'application/json'}});
let data = await response.json();
return { value: `This is joke number ${i}: ${data.joke}`, done: false };
}
let asyncIterable = {
a: 1,
b: 2,
c: 3,
d: 4,
[Symbol.asyncIterator]: function() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
if (i == keys.length) return Promise.resolve({value: null, done: true});
i++;
return fetchAsync(i);
}
}
}
};
async function process() {
for await (let item of asyncIterable) console.log(item);
}
process();
Too complex? Not at all. Let’s go over it: We have the fetchAsync function which is a normal asynchronous function that takes the variable i and returns a dad joke from the API with a number. Easy, no? You can run it like so:
let response = await fetch('https://icanhazdadjoke.com/', {headers: {'Accept': 'application/json'}});
let data = await response.json();
return { value: `This is joke number ${i}: ${data.joke}`, done: false };
}
fetchAsync(1).then((result) => {
console.log(result.value);
})
So what’s so special about this? It returns promise. Why? Because we’re calling the API using fetch—it’s better if a call to the remote server is asynchronous because we want the script to continue. That’s the reason we use promise. I use the async syntax so that I can use await inside the function—that’s it!
Now, let’s say we have an object with numbers:
let asyncIterable = {
key1: 1,
key2: 2,
key3: 3,
key4: 4,
}
We want to go over it and pass its keys to the function. This will be a problem for me. Why? First of all, this is an object. We need to go over it if we want to use it as an iteration function like earlier in the article. But then we run into another problem since the iteration function that we want to use is fetchAsync which is asynchronous because it has a call to the API. If we use a normal Symbol.iterator or a regular loop that would be great, but we wouldn’t be able to get the result back from the server.
So we need an iteration function that knows how to deal with promises, and that’s exactly what this new ES2018 feature gives us: Symbol.asyncIterator. The only difference between Symbol.asyncIterator and Symbol.iterator is that the former knows how to return promises and not regular values. In order to use Symbol.asyncIterator we need to use a special loop that contains await.
for await (let item of asyncIterable) {
console.log(item);
}
Because it contains await, it must be inside an asynchronous function.
async function process() {
for await (let item of asyncIterable) {
console.log(item);
}
}
If you want a further example—less realistic but simpler—here is some code that only returns promises:
const myObj = {
key1: 1,
key2: 2,
key3: 3,
[Symbol.asyncIterator]: function() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
let result;
if (i == keys.length) {
result = {value: null, done: true};;
} else {
result = {
value: [keys[i], this[keys[i]]],
done: false
}
}
i++;
return Promise.resolve(result);
}
}
}
};
async function runIt() {
for await (let item of myObj) {
console.log(item);
}
}
runIt();
So the question most certainly arises, why not use Promise.all or some other kind of trick? The answer is that this important addition to the standard allows us to use a synchronous writing style while doing asynchronous things, and this is the direction that JavaScript is going in. The first step was async-await that came along in ES2017, and this is the next step that throws a little bit of the magic fairy dust of async-await onto promises. Enjoy!
About the author: Ran Bar-Zik is an experienced web developer whose personal blog, Internet Israel, features articles and guides on Node.js, MongoDB, Git, SASS, jQuery, HTML 5, MySQL, and more. Translation of the original article by Aaron Raizen.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment