Asynchronous Iteration
Asynchronous Iteration
1 Asynchronous iteration
With ECMAScript 6, JavaScript got built-in support for synchronously
iterating over data. But what about data that is delivered asynchronously?
For example, lines of text, read asynchronously from a file or an HTTP
connection.
This proposal brings support for that kind of data. Before we go into it, let’s
first recap synchronous iteration.
interface AsyncIterable {
[Symbol.asyncIterator]() : AsyncIterator;
}
interface AsyncIterator {
next() : Promise<IteratorResult>;
}
interface IteratorResult {
value: any;
done: boolean;
}
2 for-await-of
The proposal also specifies an asynchronous version of the for-of loop:
for-await-of:
function createRejectingIterable() {
return {
[Symbol.asyncIterator]() {
return this;
},
next() {
return Promise.reject(new Error('Problem!'));
},
};
}
(async function () { // (A)
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
// Error: Problem!
}
})(); // (B)
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// Output:
// a
// b
3 Asynchronous generators
Normal (synchronous) generators help with implementing synchronous
iterables. Asynchronous generators do the same for asynchronous
iterables.
Use cases for calling next() several times without waiting for settlements
include:
Use case: Async generators as sinks for data, where you don’t always need
to know when they are done.
You can use await and for-await-of inside async generators. For example:
One interesting aspect of combining await and yield is that await can’t stop
yield from returning a Promise, but it can stop that Promise from being
settled:
That means that these two lines correspond (roughly) to this code:
In line (A), gen2() calls gen1(), which means that all elements yielded by
gen1() are yielded by gen2():
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// Output:
// a
// b
The operand of yield* can be any async iterable. Sync iterables are
automatically converted to async iterables, just like for for-await-of.
3.4 Errors
Async function:
(async function () {
return 'hello';
})()
.then(x => console.log(x)); // hello
(async function () {
throw new Error('Problem!');
})()
.catch(x => console.error(x)); // Error: Problem!
4 Examples
The source code for the examples is available via the repository async-
iter-demo on GitHub.
The example repo uses babel-node to run its code. This is how it configures
Babel in its package.json:
{
"dependencies": {
"babel-preset-es2015-node": "···",
"babel-preset-es2016": "···",
"babel-preset-es2017": "···",
"babel-plugin-transform-async-generator-functions": "···"
···
},
"babel": {
"presets": [
"es2015-node",
"es2016",
"es2017"
],
"plugins": [
"transform-async-generator-functions"
]
},
···
}
/**
* @returns a Promise for an Array with the elements
* in `asyncIterable`
*/
async function takeAsync(asyncIterable, count=Infinity) {
const result = [];
const iterator = asyncIterable[Symbol.asyncIterator]();
while (result.length < count) {
const {value,done} = await iterator.next();
if (done) break;
result.push(value);
}
return result;
}
Note how nicely async functions work together with the mocha test
framework: for asynchronous tests, the second parameter of test() can
return a Promise.
/**
* Creates an asynchronous ReadStream for the file whose name
* is `fileName` and feeds it into an AsyncQueue that it returns.
*
* @returns an async iterable
*/
function readFile(fileName) {
const queue = new AsyncQueue();
const readStream = createReadStream(fileName,
{ encoding: 'utf8', bufferSize: 1024 });
readStream.on('data', buffer => {
const str = buffer.toString('utf8');
queue.enqueue(str);
});
readStream.on('end', () => {
// Signal end of output sequence
queue.close();
});
return queue;
}
Step 2: Use for-await-of to iterate over the chunks of text and yield lines of
text.
/**
* Turns a sequence of text chunks into a sequence of lines
* (where lines are separated by newlines)
*
* @returns an async iterable
*/
async function* splitLines(chunksAsync) {
let previous = '';
for await (const chunk of chunksAsync) {
previous += chunk;
let eolIndex;
while ((eolIndex = previous.indexOf('\n')) >= 0) {
const line = previous.slice(0, eolIndex);
yield line;
previous = previous.slice(eolIndex+1);
}
}
if (previous.length > 0) {
yield previous;
}
}
Step 3: combine the two previous functions. We first feed chunks of text
into a queue via readFile() and then convert that queue into an async
iterable over lines of text via splitLines().
/**
* @returns an async iterable
*/
function readLines(fileName) {
// `queue` is an async iterable
const queue = readFile(fileName);
return splitLines(queue);
}
Lastly, this is how you’d use readLines() from within a Node.js script:
(async function () {
const fileName = process.argv[2];
for await (const line of readLines(fileName)) {
console.log('>', line);
}
})();
const rs = openReadableStream();
for await (const chunk of rs) {
···
}
6 The specification of asynchronous
iteration
The spec introduces several new concepts and entities:
If you want to understand how async generators work, it’s best to start with
Sect. “AsyncGenerator Abstract Operations”. They key to understanding
async generators is to understand how queuing works.
csp.go(function* () {
var table = csp.chan(); // (A)
player defines a “process” that is instantiated twice (in line (B) and in line
(C), via csp.go()). The processes are connected via the “channel” table,
which is created in line (A) and passed to player via its second parameter.
A channel is basically a queue.
In line (A), we create a stream of click events via fromEvent(). These events
are then filtered so that there is at most one event per second. Every time
there is an event, scan() counts how many events there have been, so far.
In the last line, we log all counts.
9 Further reading
“Streams API FAQ” by Domenic Denicola (explains how streams and
asynchronous iteration are related; and more)
“Why Async Iterators Matter” (slides by Benjamin Gruenbaum)
Background:
Ashok Patil
Marcelo Lazaroni −
6 years ago
Hmm.. This really doesn't look necessary at all. It seems like a needless addition
to the language trying to provide new tools to solve a problem that can already
be solved with our current tools. It will be a shame if JavaScript goes down the
path of bloating the language.
6△ ▽ Reply
Benjamin Strauß −
6 years ago
"We need some kind of support for asynchronous sequences of data (which are
different from streams!). That much is obvious."
But why do we need 3 separate APIs for basically the same effect
(AsyncIterator/Observable/Stream)? There is no reason to introduce all of these.
The only argument I know of is that they have different life cycles and different
number of subscribers. But that argument goes away if you look at how this is
implemented in Dart.
https://www.dartlang.org/ar...
There was a talk about exactly that, two years ago at JSConf:
Thanks for the link to the talk, I’ll take a look. Until now, my impression was
that streams (as in streams of bytes or characters) catered for different use
cases, but I may be wrong.
[I’ve edited the post slightly in response to your comment, hopefully making
it clearer what I mean.]
1△ ▽ Reply
Benjamin Strauß > Axel Rauschmayer −
6 years ago edited
The whatwg Stream proposal is indeed intended for low level streams. My
point is if you look at it fundamentally, all of these separate tc39/whatwg
proposals (AsyncIterator/Stream/Observable) basically do the same thing:
iterate asynchronously over a collection of data.
The fact that one flavor of that collection are UI events which have no
defined lifecycle and 1-n consumers (Observable) and the other kind is a
low level bytestream with a well defined lifecycle and a single consumer
(Stream), shouldn't really make a difference API wise. It should be an
implementation detail of a higher API, just like Streams in Dart which
unify all of that (that Dart article explains it really well, I basically just
repeat what's written there :) ).
What would happens if you would like to use all your great higher order
functions on these 3 APIs. Do all have map/filter/reduce? Can I return all of
them from async generator functions (only AsyncIterator at the moment?)?
What if I have my own higher order function, are all 3 compatible with it?
I think a single class with a single set of functions that generalises all
flavors, would be a win for everyone. But I think it might be too late for
that now, given how far these proposals are in the process. :(
△ ▽ Reply
The "async pull" model is nice to have when you are doing progressive
downloads (like paging) from an expensive/server-limited resource, so you
want to avoid requests unless you *know* you need them.
1△ ▽ Reply
Pijamoney Get Free Bitcoins −
6 years ago
I refuse to absorb all these bloated redundant pedantic reinventions of the
wheel that comes after ES5. The cognitive overhead is too damn high.
4△ ▽ 2 Reply
coloured_chalk > Pijamoney Get Free Bitcoins −
6 years ago
agreed, I feel stupid looking on those `async function*`'s which `await yeld*
...`. this too shall pass after some practice
Domenic Denicola −
6 years ago
A few issues.
> We need some kind of support for asynchronous sequences of data (which are
different from streams!).
You seem to have missed the connection between async iterables and streams.
See https://github.com/whatwg/s.... In short, async iterables are "different" from
streams in the same sense that sync iterables are "different" from arrays. Saying
that one is redundant with the other is just a misunderstanding of the role of
abstraction in software development.
“That isn't really true. Async generators make [converting legacy APIs to
async iteration] trivial.”
I’m basing my assertion on the following code – I couldn’t implement
readFile() as an async generator and I needed to implement an async queue.
https://github.com/rauschma...
△ ▽ Reply
John Johnson > Axel Rauschmayer −
5 years ago edited
I just made an example of your readfile() method with an async generator
here https://github.com/johnsonj.... It's based off Benjamin Gruenbaum's
idea of the once function for mixing promises and event emitter.
1△ ▽ 1 Reply
Benjamin Gruenbaum −
6 years ago
Here is a talk I gave about it a while ago that demonstrates how to easily build
the Rx code in "example #2" in async iterators https://docs.google.com/pre... - I
don't think the two proposal compete but rather complement each other.
1△ ▽ Reply
Axel Rauschmayer > Benjamin Gruenbaum −
6 years ago
Very useful, thanks!
1△ ▽ Reply
danvdk −
6 years ago edited
It's always frustrated me that node doesn't have any easy equivalent of Python's
import csv
for line in csv.reader(open('file.csv')):
# ...
What's more, those three lines of Python tend to be significantly faster than
anything build with node streams. A fast, expressive way to read files line-by-
line without loading them into memory all at once would go a long way towards
letting me replace Python scripts with node ones.
1△ ▽ Reply
Tahor Sui Juris −
3 years ago
Hello,
function readKeyWords(path) {
return fs.readFileSync(path, 'utf-8')
.split('\n');
}
△ ▽ Reply
This comment was deleted. −
Avatar
Axel Rauschmayer > Guest −
5 years ago
△ ▽ Reply
b
vmb −
5 years ago
i certainly agree we need it. i had to emulate it just recently, where i developed a
semantic-database layer (over MongoDB) because i wanted to be able to iterate
comfortably over a query results (each result is, of course, a "promised" result).
It would have been more elegant had the language helped a little more (i used
typescript btw).
△ ▽ Reply
Andrew Shaw Care −
6 years ago
I created a toy project to tackle this same problem a while ago:
https://github.com/andrewsh...
△ ▽ Reply