Generator Generation

17 December 2025

Updated: 24 December 2025

So I was just going to write a short post about this handy function I made, but I thought it would be a nice opportunity to build a little bit more of an understanding around the topic of generators more broadly

Our end goal is going to be to define an abstraction that will allow us to convert any Callback-Based API into an Async Generator. Now, if those words don’t mean much to you, then welcome to the other side of JavaScript. If you’re just here for the magic function, however, feel free to skip to the end

Promises

Before diving into the complexity of generators, we’re going to quickly kick off with a little introduction to Promises and how they relate to async/await and callback-based code

Promises are used to make async code easier to work with and JavaScript has some nice syntax - like async/await that makes code using promises easier follow and understand. They’re also the common way to represent async operations which is exactly what we’re going to use them for

Before we can dive right into implementing our function for creating an AsyncGenerator from a callback based API, it’s important to understand how we might go about wrapping a callback based API into a Promise

Creating Promises from Callbacks

Often we end up in cases where we’ve got some code that is callback based - this is a function, for example setTimeout, that will invoke the rest of our code asynchronously when some task is done or event is received

A simple example of a callback based function is setTimeout which will resume the execution of our code after some specified amount of time:

1
setTimeout(() => {
2
console.log('this runs after 1000ms')
3
}, 1000)

A common usecase is to convert this to a Promise so that consumers can work with this using async functions and awaiting the relevant function call

The basic method for doing this consists of returning a Promise and handling the rejection or resolution within the callback. For example, we can create a promise-based version of setTimeout using this approach:

1
function sleep(timeout: number) {
2
return new Promise((resolve, _reject) => {
3
setTimeout(resolve, timeout)
4
})
5
}

This promise will now resolve when setTimeout calls the resolve method that’s been passed to it. This allows us to turn some callback based code like this:

1
setTimeout(() => {
2
console.log('done')
3
}, 1000)

Into this:

1
await sleep(1000)
2
console.log('done')

Granted, this isn’t a huge difference - the value of this comes from when we have multiple of these kinds of calls nested within each other. Callback code is notorious for its tendency towards chaos. My rule of thumb on this is basically “less indentation is easier to understand”. And if we can avoid indentation and keep our code flat we can focus on the essential complexity of our application and not the cognitive load that comes with confusing scope, syntax, and callbacks

This is such a common problem in the JavaScript world, that Node.js even has a builtin function node:util called promisify that converts Node.js style callback functions into promise-based ones

Async/Await

When working with promises, it’s useful to define our methods using the async keyword, this allows us to work with a Promise using await and not have any callbacks

So we can have our function below, which can await the sleep function and it would be defined like so:

1
async function doWork(){
2
await sleep(5000)
3
console.log('fine, time to work now')
4
}

The doWork function returns Promise, this is because the async keyword is some syntax sugar for creating a Promise

Promises Vs Async

For the sake of understanding, all that the async keyword does allow us to remove the Promise construction from our function - async functions are simply functions that return a Promise - these are alternative syntax for the same thing - so, the following two functions are the same:

Using async:

1
async function getNumber() {
2
return 5
3
}

Using an explicit Promise:

1
function getNumber() {
2
return new Promise((resolve) => resolve(5))
3
}

Promise.withResolvers

Another pattern that often comes us is the need to reach into the Promise constructor and grab onto its resolve and reject methods and pass them around so that we can “remotely” compelete a Promise, as per MDN, the common pattern for doing this looks something like so:

1
function withResolvers<T>() {
2
let resolve: (v: T) => void = () => {}
3
let reject: (error: unknown) => void = () => {}
4
5
new Promise<T>((res, rej) => {
6
resolve = res
7
reject = rej
8
})
9
10
return {
11
resolve,
12
reject,
13
}
14
}

This method is also a recent addition to the Promise class via Promise.withResolvers, but for cases where it’s not - the above should serve the equivalent purpose

Now that we’ve got an understanding of Promises, it’s time to talk about Iterators and Generators

Iterators and Generators

Iterators and generators enable iteration to work in JavaScript and are what lies behind objects that are iterable by way of a for ... of loop

Iterator

An iterator is basically an object that will return a new value whenever its next method is called

A simple iterator can be defined as an object that has a next method that returns whether it’s done or not.

1
function countToIterator(max: number): Iterator<number> {
2
let value = 0
3
4
const iterator: Iterator<number> = {
5
next() {
6
value++
7
return {
8
value,
9
done: value === max,
10
}
11
},
12
}
13
14
return iterator
15
}

JavaScript uses the Symbol.iterator property to reference this object and therefore makes it possible for the language for ... of loop to iterate through this object. We can use the countToIterator’s returned iterator to define an Iterable

What a mouthful right?? - But the implementation is actually easier than the description:

1
function countTo(max: number): Iterable<number> {
2
const iterator = countToIterator(max)
3
4
return {
5
[Symbol.iterator]() {
6
return iterator
7
},
8
}
9
}

Once we’ve got this, we can use the countTo in a loop:

1
// counts from 1 to 5
2
for (const v of countTo(5)) {
3
console.log('count', v)
4
}

Could this be an array? Maybe. Arrays are really just a special case of an iterator. More generally, iterators are cool because they don’t have to have a fixed endpoint, or even a fixed list of values. For example, we can create a different iterator that counts until a random point by modifying how the next function works:

1
function randomlyStopCounting(): Iterable<number> {
2
let value = 0
3
4
const iterator: Iterator<number> = {
5
next() {
6
value++
7
return {
8
value,
9
done: Math.random() > 0.8,
10
}
11
},
12
}
13
14
return {
15
[Symbol.iterator]() {
16
return iterator
17
},
18
}
19
}
20
21
22
for (const v of randomlyStopCounting()) {
23
console.log('count', v)
24
}

This is used the same as above, but the point at which this will return isn’t really known beforehand. This dynamic behavior can come from lots of different places and not just from Math.random and it can allow some really interesting behaviors

Generators

Defining the above iterators is fun and all, but it’s quite messy. The higher-order syntax for defining these kinds of iterators is using Generators

(I know, what’s with all these words right??)

Okay, so generator functions are functions that return a special iterator called a Generator. If we weren’t into the territory of weird syntax already - generators are defined using the function* keyword, and use the yield keyword to provide the next value. So we can rewrite our countTo function using a generator function and it would look like this:

1
function* countTo(max: number): Generator<number> {
2
let value = 0
3
4
while (value < max) {
5
value++
6
yield value
7
}
8
}

That’s actually way nicer to read right? When we yield a value the flow is delegated to the loop body, just like in the case of a normal iterator, which means the consumer can actually end the iteration early, like:

1
for (const v of countTo(5)) {
2
console.log('count', v)
3
if (v == 3) {
4
break
5
}
6
}

This also applies for the iterators above, I just find it so much more interesting in this case because there’s no concept that someone is going to “call the next method again” which is so transparent in the iterator example above

Async Generators

Now, we’re taking one more step - what if I wanted to do some long running task between each yield? This could be anything from waiting for a Promise to resolve, or a network request, or some user event (oh wow - there’s an idea for multistep forms!)

Async Generators enable us to use promises in our iterators. Let’s take a look at how we might define an async version of our countTo generator above:

1
async function* countToAsync(max: number): AsyncGenerator<number> {
2
let value = 0
3
4
while (value < max) {
5
await sleep(1000)
6
value++
7
yield value
8
}
9
}

Almost exactly the same right? Aside from the sneaky async keyword and the await in the loop, this is pretty similar to the sync version above. Using this also has a new little twist, can you spot it?:

1
for await (const v of countToAsync(5)) {
2
console.log('async count', v)
3
}

Interesting right? We’re now using a for await ... of loop. If you were to run this, you’d also notice that there’s a little pause between each value being logged

Unwrapping the Generator

Now that we’ve seen what the inside of an iterator looks like - it’s time to open the box and see what generators have inside

Let’s start with the sync version

Inside a Sync Generator

So if we redefine our countTo generator without using the function* and yield syntax sugar, we’ll see something like this:

1
function countTo(max: number): Generator<number> {
2
let value = 0
3
4
const generator: Generator<number> = {
5
[Symbol.iterator]() {
6
return generator
7
},
8
next() {
9
value++
10
return {
11
value,
12
done: value === max,
13
}
14
},
15
return() {
16
return undefined
17
},
18
throw(e) {
19
throw e
20
},
21
}
22
23
return generator
24
}

This looks very similar to an iterator - and that’s because it is!

The Generator type inherits from the Iterator and needs an additional return and throw methods. The return method allows the generator to handle any cleanup once the consumer is done iterating. The throw allows any handling of errors and any other cleanup tasks

Inside an Async Generator

The async version of the above is almost identical - but we just sprinkle the async keyword around to make the respective methods all return a Promise as this is what the AsyncGenerator requires:

1
function countToAsync(max: number): AsyncGenerator<number> {
2
let value = 0
3
4
const generator: AsyncGenerator<number> = {
5
[Symbol.asyncIterator]() {
6
return generator
7
},
8
async return() {
9
return undefined
10
},
11
async next() {
12
await sleep(1000)
13
value++
14
return {
15
value,
16
done: value === max,
17
}
18
},
19
async throw(e) {
20
throw e
21
},
22
}
23
24
return generator
25
}
26
27
for await (const v of countToAsync(5)) {
28
console.log('async count', v)
29
}

Just like when defining the Async Generator before, we’re just calling sleep between each return value. We’ve also made the return and throw methods async as well

Creating Generators from Callback Functions

Well, it’s been a long way, but we finally have all the tools we need to turn a callback based method into an iterator. So far, we’ve been using setTimeout for our callbacks, but generators return multiple values. We’re going to create a little modified version of setInterval for this so that we can play around

The version we’ll define is called countInterval and will emit a new number until the given value and then stop, this looks like so:

1
function countInterval(
2
max: number,
3
onValue: (num: number) => void,
4
onDone: () => void,
5
) {
6
let value = 0
7
const interval = setInterval(() => {
8
if (value === max) {
9
clearInterval(interval)
10
onDone()
11
return
12
}
13
14
value++
15
onValue(value)
16
}, 1000)
17
}

And the usage looks like this:

1
countInterval(
2
5,
3
(v) => console.log('count interval', v),
4
() => console.log('count interval done'),
5
)

Assume for whatever reason that we want to be able to take functions like this and turn them into generators. In general, we can go about the process of manually defining a generator, but that’s a little tedious and requires managing a lot of internal state. It would be nice if we could do this without having to manage the intricate details of a custom generator. We basically want to define countInterval such that it looks like this:

1
function countIntervalGenerator(max: number): AsyncGenerator<number> {}

For now, let’s assume we’ve got a method called createGenerator that returns everything we need in order to hook up a generator and return it, this looks something like this:

1
function countIntervalGenerator(max: number): AsyncGenerator<number> {
2
const { generator, next, done } = createGenerator<number>()
3
4
countInterval(max, next, done)
5
6
return generator
7
}

And our new generator can be used just as usual:

1
for await (const value of countIntervalGenerator(5)) {
2
console.log('count interval generator', value)
3
}

Defining this wonderful createGenerator function combines what we’ve learnt about promises and generators to get this:

1
function createGenerator<T>() {
2
let current = Promise.withResolvers<T>()
3
let final = Promise.withResolvers<void>()
4
5
const generator: AsyncGenerator<T> = {
6
[Symbol.asyncIterator]() {
7
return generator
8
},
9
10
async return() {
11
const value = await final.promise
12
13
return {
14
done: true,
15
value
16
}
17
},
18
19
async next() {
20
const value = await current.promise
21
return {
22
done: false,
23
value,
24
}
25
},
26
27
async throw(e) {
28
throw e
29
},
30
}
31
32
const next = (value: T) => {
33
current.resolve(value)
34
current = Promise.withResolvers()
35
}
36
37
const done = () => {
38
final.resolve()
39
}
40
41
return {
42
next,
43
done,
44
generator,
45
}
46
}

This isn’t doing anything that we haven’t covered before. The only interesting bit (in my opinion) is how the next handler creates a new promise on each iteration - this implementation probably has some weirdness due to that so in practice you probably want to handle that edge case somewhat. This could maybe be done by updating the next function to take a parameter to indicate if it will emit a new value or not but in practice it’s not always easy to figure that out - and in many cases you just don’t know

That all being said, I think this implementation should make it clear how these pieces all fit together - there’s a lot more detail that can be had in this discussion since each of these topics are fairly deep - but I hope this post was - if not useful - then at least interesting

References and Further Reading