Streaming fetches are supported in Chrome, Edge, and Safari, and they look a little like this:

async function getResponseSize ( url ) { const response = await fetch ( url ) ; const reader = response . body . getReader ( ) ; let total = 0 ; while ( true ) { const { done , value } = await reader . read ( ) ; if ( done ) return total ; total += value . length ; } }

This code is pretty readable thanks to async functions (here's a tutorial if you're unfamiliar with those), but it's still a little clumsy.

Thankfully, async iterators are arriving soon, which makes it much neater:

async function getResponseSize ( url ) { const response = await fetch ( url ) ; let total = 0 ; for await ( const chunk of response . body ) { total += chunk . length ; } return total ; }

Async iterators are available in Chrome Canary if you launch it with the flag --js-flags=--harmony-async-iteration . Here's how they work, and how we can use them to make streams iterate…

Async iterators

Async iterators work pretty much the same as regular iterators, but they involve promises:

async function example ( ) { const iterator = createNumberIterator ( ) ; iterator . next ( ) ; iterator . next ( ) ; iterator . next ( ) ; iterator . next ( ) ; const asyncIterator = createAsyncNumberIterator ( ) ; const p = asyncIterator . next ( ) ; await p ; await asyncIterator . next ( ) ; await asyncIterator . next ( ) ; await asyncIterator . next ( ) ; }

Both types of iterator have a .return() method, which tells the iterator to end early, and do any clean-up it needs to do.

Iterators & loops

It's fairly uncommon to use iterator objects directly, instead we use the appropriate for loop, which uses the iterator object behind-the-scenes:

async function example ( ) { for ( const item of thing ) { } for await ( const item of asyncThing ) { } }

The for-of loop will get its iterator by calling thing[Symbol.iterator] . Whereas the for-await loop will get its iterator by calling asyncThing[Symbol.asyncIterator] if it's defined, otherwise it will fall back to asyncThing[Symbol.iterator] .

For-await will give you each value once asyncIterator.next() resolves. Because this involves awaiting promises, other things can happen on the main thread during iteration. asyncIterator.next() isn't called for the next item until your current iteration is complete. This means you'll always get the items in order, and iterations of your loop won't overlap.

It's pretty cool that for-await falls back to Symbol.iterator . It means you can use it with regular iterables like arrays:

async function example ( ) { const arrayOfFetchPromises = [ fetch ( '1.txt' ) , fetch ( '2.txt' ) , fetch ( '3.txt' ) ] ; for ( const item of arrayOfFetchPromises ) { console . log ( item ) ; } for await ( const item of arrayOfFetchPromises ) { console . log ( item ) ; } }

In this case, for-await takes each item from the array and waits for it to resolve. You'll get the first response even if the second response isn't ready yet, but you'll always get the responses in the correct order.

Async generators: Creating your own async iterator

Just as you can use generators to create iterator factories, you can use async generators to create async iterator factories.

Async generators a mixture of async functions and generators. Let's say we wanted to create an iterator that returned random numbers, but those random numbers came from a web service:

async function * asyncRandomNumbers ( ) { const url = 'https://www.random.org/decimal-fractions/?num=1&dec=10&col=1&format=plain&rnd=new' ; while ( true ) { const response = await fetch ( url ) ; const text = await response . text ( ) ; yield Number ( text ) ; } }

This iterator doesn't have a natural end – it'll just keep fetching numbers. Thankfully, you can use break to stop it:

async function example ( ) { for await ( const number of asyncRandomNumbers ( ) ) { console . log ( number ) ; if ( number > 0.95 ) break ; } }

Live demo

Like regular generators, you yield values, but unlike regular generators you can await promises.

Like all for-loops, you can break whenever you want. This results in the loop calling iterator.return() , which causes the generator to act as if there was a return statement after the current (or next) yield .

Using a web service to get random numbers is a bit of a silly example, so let's look at something more practical…

Making streams iterate

Like I mentioned at the start of the article, soon you'll be able to do:

async function example ( ) { const response = await fetch ( url ) ; for await ( const chunk of response . body ) { } }

…but it hasn't been spec'd yet. So, let's write our own async generator that lets us iterate over a stream! We want to:

Get a lock on the stream, so nothing else can use it while we're iterating.

Yield the values of the stream.

Release the lock when we're done.

Releasing the lock is important. If the developer breaks the loop, we want them to be able to continue to use the stream from wherever they left off. So:

async function * streamAsyncIterator ( stream ) { const reader = stream . getReader ( ) ; try { while ( true ) { const { done , value } = await reader . read ( ) ; if ( done ) return ; yield value ; } } finally { reader . releaseLock ( ) ; } }

The finally clause there is pretty important. If the user breaks out of the loop it'll cause our async generator to return after the current (or next) yield point. If this happens, we still want to release the lock on the reader, and a finally is the only thing that can execute after a return .

And that's it! Now you can do:

async function example ( ) { const response = await fetch ( url ) ; for await ( const chunk of streamAsyncIterator ( response . body ) ) { } }

Live demo

Releasing the lock means you can still control the stream after the loop. Say we wanted to find the byte-position of the first J in the HTML spec…

async function example ( ) { const find = 'J' ; const findCode = find . codePointAt ( 0 ) ; const response = await fetch ( 'https://html.spec.whatwg.org' ) ; let bytes = 0 ; for await ( const chunk of streamAsyncIterator ( response . body ) ) { const index = chunk . indexOf ( findCode ) ; if ( index != - 1 ) { bytes += index ; console . log ( ` Found ${ find } at byte ${ bytes } . ` ) ; break ; } bytes += chunk . length ; } response . body . cancel ( ) ; }

Live demo

Here we break out of the loop when we find a match. Since streamAsyncIterator releases its lock on the stream, we can cancel the rest of it & save bandwidth.

Note that we don't assign streamAsyncIterator to ReadableStream.prototype[Symbol.asyncIterator] . This would work – allowing us to iterate over streams directly, but it's also messing with objects we don't own. If streams become proper async iterators, we could end up with weird bugs if the spec'd behaviour is different to ours.

A shorter implementation

You don't need to use async generators to create async iterables, you could create the iterator object yourself. And that's what Domenic Denicola did. Here's his implementation:

function streamAsyncIterator ( stream ) { const reader = stream . getReader ( ) ; return { next ( ) { return reader . read ( ) ; } , return ( ) { reader . releaseLock ( ) ; return { } ; } , [ Symbol . asyncIterator ] ( ) { return this ; } } ; }