Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
Even more articles at http://thewebdev.info/
Asynchronous code is a very important part of JavaScript. We need to write lots of asynchronous code, since JavaScript is a single-threaded language. If we run everything line by line, then code that takes longer to run will hold up the program, causing it to freeze.
To write asynchronous code easily, JavaScript has promises, which are chainable and can be run in sequence without nesting.
ES2017 introduced the async
and await
syntax for chaining promises, which makes everything easier. However, there was still no way to run promises sequentially, iterating them in sequence. This means that running lots of promises in a sequence is still a problem with no solution.
Fortunately, in ES2018, we finally have a for-await-of
loop that we can use with async
functions that were introduced in ES2017. It works with any iterable objects, like the synchronous for...of
loop. This means that we can iterate through objects like Maps, Sets, NodeLists, and the arguments
object with it.
Also, it can any object with the Symbol.iterator
method, which returns a generator to let us do the iteration.
for-await-of Loop
We can use the for-await-of
loop as follows:
let promises = [];
for (let i = 1; i <= 10; i++) {
promises.push(new Promise((resolve) => {
setTimeout(() => resolve(i), 1000);
}));
}
(async () => {
for await (let p of promises) {
const val = await p;
console.log(val);
}
})();
In the example above, we added ten promises by defining them in the for
loop and then pushing them to the promises
array. Then we run the promises by defining an async
function and then using the for-await-of
loop to loop through each promise and run them one at a time.
The await
in the for-await-of
loop will get us the resolved value from the promise and the console.log
will log the resolved value.
Like other async
functions, it can only return promises.
The syntax of the for-await-of
loop is the same as any other for...of
loop. The let p
is the variable declaration and promises
is the promises
array we’re iterating through.
The code above works like Promise.all
, the code is run in parallel, so we see one to ten after one second.
Async Iterables
Alternatively, we can define our own asynchronous iterable object if we define an object with the Symbol.asyncIterator
method. It’s a special method that returns a generator that lets us iterate through it with the for-await-of
loop.
For example, we can write:
let asyncIterable = {
[Symbol.asyncIterator]() {
return {
i: 1,
next() {
if (this.i <= 10) {
return new Promise((resolve) => {
setTimeout(() => resolve({
value: this.i++,
done: false
}), 1000);
});
}
return Promise.resolve({
done: true
});
}
};
}
};
(async () => {
for await (let p of asyncIterable) {
const val = await p;
console.log(val);
}
})();
The code above will return a generator, which returns a new promise as the for-await-of
loop runs.
In the Symbol.asyncIterator
method, we have a next
function, which is required if we want to make an asynchronous iterable object. Also, we have the i
property to keep track of what we iterated through.
In the promise that we return, we have the setTimeout
with a callback function that resolves to an object with the value
we want to resolve, and the property done
. The done
property is false
if the iterator should keep on returning new values for the for-await-of
loop and true
otherwise.
Async Generator
We can make the code shorter with the yield
keyword to make a generator function to return a generator. Generators are another kind of iterable object which can be used with the for-await-of
loop.
For example, we can write:
The keyword function*
denotes that our function only returns a generator. async
means that the generator runs asynchronously. Inside the asyncGenerator
function, we have the yield
keyword. The yield
keyword gets us the next item from the generator as we loop through the generator with the for-await-of
loop.
The yield
keyword only works with top-level code. It doesn’t work inside callback functions.
If we run the code above, we should get one to ten outputted.
If we write something like this:
async function* asyncGenerator() {
for (let i = 1; i <= 10; i++) {
setTimeout(() => {
yield i;
}, 1000);
}
}
We’ll get a SyntaxError
.
An object can be made iterable with if we define a Symbol.asyncIterator
method which is set to a generator function.
For example, we can write:
async function* asyncGenerator() {
for (let i = 1; i <= 10; i++) {
yield i;
}
}
(async () => {
for await (let p of asyncGenerator()) {
const val = await p;
console.log(val);
}
})();
Then we get one to ten logged in the console.log
output.
It works like the asyncIterable
object we defined above, except that the code is much shorter. Also, we can’t use the yield
keyword on callbacks so we have to write it as we have above in that case.
Finally, we can write the following to loop through promises directly:
The await
keyword actually waits for each promise to resolve until running the next one, so we get each number showing up after one second when we run the code.
Asynchronous iteration solves the promise of running many promises sequentially. This feature is available in ES2018 or later with the introduction of the for-await-of
loop and asynchronous iterators and generators. With these the issue of iterating through asynchronous code has been solved.