Async generators (and their close friend for await
) are a really cool feature of modern JavaScript that you can use when a loop requires asynchronous iteration. What the heck is that? Well let’s look at a regular for...of
loop:
1 | const array = [1, 2, 3]; |
Nothing weird here, we’re looping over an array and printing each element. An array is a synchronous data structure, so we can loop over it very simply. But what about asynchronous data, like say the fetch
API to get data from a HTTP endpoint. A simple implementation of looping over that data is not much more complex:
1 | const res = await fetch("https://whatever.com/api"); |
We’re still using for...of
and being synchronous in our loop so let’s add one more factor: we’re using an API that uses pagination of some sort, which is pretty much every API ever made since it’s expensive to load giant datasets. A simple pagination-based iteration might look something like this:
1 | let data = []; |
This is sort of async iteration but it suffers from a major shortcoming: you have to comingle the iteration code and the data fetching in the same loop as each page is ephemeral - or else select all the pages and allocate a monster array before you can deal with the data. Neither of those are great options, so let’s try an async generator function instead. An async generator is a function that returns an async iterable that you can loop over. Like other iterable and enumerable types (such as IEnumerable
in C#), async iterables in JS are essentially an object with a next
function to get the next thing in the iterable. This has several interesting side effects:
- You cannot have random access to an iterable (it is forward-only)
- Because it is forward only, memory need not be allocated for the entire iterable at the same time so it’s easily possible to have an iterable that has a nearly unlimited of iterations without running out of memory.
- If you stop iterating, the rest of the elements in the iterable are never evaluated. For our pagination example, this means that if we stop reading at page 2, but page 3 exists, we’ll never fetch page 3 from the server.
For my own sanity I’m going to drop into TypeScript here to illustrate the types that we’re passing around :)
1 | // This paginateAsync function implements generic pagination functionality over an arbitrary API, |
So now you can call a paginated API and treat the result as if it were a non-paginated loop. Pretty neat, right? Even better, you can use this in all modern browsers. (IE ain’t modern, folks…)
Again for my C# readers: this is pretty similar conceptually to C#’s IAsyncEnumerable
construct and can be used in similar circumstances.