Sitemap
Clairehap

Clairehap

JavaScript Generator Functions: Multiple Outputs, Multiple Case Uses

11 min readJun 1, 2026

--

One of the first concepts introduced to JavaScript beginners is the idea of data types. After learning about the primitive data types (Number, Bigint, String, Boolean, Undefined, Null and Symbol), I remember vividly diving into learning about complex data types: Objects, Arrays and of course, Functions.

I can’t tell you how many add function examples I saw as a new student!

But what is a Function?

MDN defines a function generally as:

A “subprogram” that can be called by code either internally or externally.

Functions can have a name, return, and parameters optionally. They have a body wrapped in brackets (unless your arrow function spans one line) and always have a local (function) scope. They could be immediately invoked (IIFEs), could create closures (which I wrote on, for beginners), could be constructors, could be helpers to a greater function, they could be function declarations or function expressions- but essentially, they’re highly flexible in what they can do and can help to manipulate data quickly and reliably.

Though functions return “function” when the typeof operator is used, functions have all of the abilities one would expect of an object (ex: they can be passed to other functions, assigned to variables, can become properties on objects, and can have properties and methods similar to any other object).

What sets them apart from other objects is their ability to be called, and functions themselves are referred to as first-class objects in JavaScript (not all languages allow this, for those curious).

Press enter or click to view image in full size
It’s OK functions, JavaScript knows you deserve only the best!

JavaScript reads code from top to bottom and right to left, and the local scope of a function is not an exception to the rules of this control flow. Internally, hoisting occurs prior to code execution and when a function runs- it is generally understood that when called, a function will execute the entirety of the code within in it unless hitting an early return.

And this is where Generator Functions come into play- they defy the aforementioned norm of how most functions operate.

What is a Generator Function?

While most functions resolve to a singular output (or if they have no return, simply perform actions dictated by code within their bodies and then end)- a generator function has the ability to return multiple outputs. It can stop itself, unlike other functions that must run unless they hit a return. Perhaps even stranger, it can pick back up on a paused process and resume right where it left off.

Every generator function has inherited methods that come from the GeneratorFunction object. Each and every generator function is actually a new instance of an object, also called a child, created by this parent. You might think that this sounds oddly familiar and for good reason- it’s an excellent example of Prototypal Inheritance.

A bit of a sidebar: If you’re curious about how the structure of massive functions like the GeneratorFunction object looks, but slightly vexed that it’s not a global object and thus less easy to view- I highly recommend pulling up a console and plopping in the code:

const GeneratorFunction = function* () {}.constructor;
const GeneratorPrototype = GeneratorFunction.prototype.prototype;
console.log(GeneratorPrototype);

What you’ll see is a huge object full of methods! Take note of how each key is a string representing a method (here I’ve expanded the method next) and the values are invocations of the methods themselves:

Press enter or click to view image in full size

We use inherited methods frequently when we use syntax like this: ourThing.methodName() or perhaps to give you a more familiar example:

// we use the methods provided via inheritance constantly
const array = [1, 2, 3];
// .slice() is a method located on Array.prototype
const slicedArr = array.slice(1);
// if you logged this on the console, you'd see all the Array methods!
console.log(Array.prototype)

While it’s also fun to scour source documents and you can technically find all of the details surrounding this object at v8/src/builtins on github if you just search the page for “generator”, I find looking at the object itself to be more beneficial.

Anatomy of a Generator Function:

All generator functions are created similarly to functions, but with a few caveats. The first difference is that at declaration a star behind “function” designates them as a generator- it should be noted that while they can be initialized as values to variables, generator functions cannot be created with arrow syntax. Here are two examples of initialization:


// Two ways to create generators, both equally excellent.
// Only difference between the two would be that the function
// declareYourself would be hoisted (and could be accessed
// before declaration) while expressoYourself would throw
// a ReferenceError stating it could not be accessed
// before initialization.

// function expression
const expressoYourself = function* () {
yield "mocha";
yield "latte";
yield "cafe au lait";
};

// vs function declaration
function* declareYourself() {
yield "I do declare!";
yield "Declaring that I am declaration!";
// obligatory office quote
yield "I DECLARE BANKRUPTCY!";
}

Let’s check out an example where we’re actually getting our yields returned to us: Here is a generator function, aptly named “generator” that’s going to give us multiple different values at different times:

// function needs to be declared with *
function* generator() {
// our various yields, to be sent back at different times
yield "woah there, send this back";
yield "also this, thanks!";
yield "don't forget this!"
}

// set up a variable to the call of generator
// if you're curious as to why we'll talk logic in a minute
const gen = generator();

// each call is like turning a new page on a book,
// returning a completely different yield
console.log(gen.next().value) // => "woah there, send this back"
console.log(gen.next().value) // => "also this, thanks!"
console.log(gen.next().value) // => "don't forget this!"

We see in the body of this function that we have multiple yields and you might be thinking, “Yield is similar to return, but somehow we’re performing multiple returns and not exiting the function-”

Then, looking down at our console logs (which contain what a correct method to recieve a value would look like) and seeing that instead of calling generator() directly with generator().next().value we’ve created a variable called gen to hold the invocation- you might be thinking, “That seems a bit unnecessary, what if we cut that out and shortened the code?”

Let’s try it out, and see what happens-

// function needs to be declared with *
function* generator() {
// our various yields, to be sent back at different times
yield "woah there, send this back";
yield "also this, thanks!";
yield "don't forget this!"
}

// ladies and gentlemen, we have a problem- but what is our problem?
console.log(generator().next().value) // => "woah there, send this back"
console.log(generator().next().value) // => "woah there, send this back"
console.log(generator().next().value) // => "woah there, send this back"

You’ll notice we’ve appeared to hit some sort of snag. To figure out what’s happening, we need to know a bit more about how these functions function!

Get Clairehap’s stories in your inbox

Join Medium for free to get updates from this writer.

Reflect for a moment on how generator functions are instances. We made one instance, one gen , but what if there were other gens? Would they all return the same yield at the same time? Let’s find out:

// function needs to be declared with *
function* generator() {
// our various yields, to be sent back at different times
yield "woah there, send this back";
yield "also this, thanks!";
yield "don't forget this!"
}

// lets create three instances of our generator function
const gen = generator();
const gen2 = generator();
const gen3 = generator();

// grab the first yield of the first instance
console.log(gen.next().value) // => "woah there, send this back"
// now the first yield of the second instance
console.log(gen2.next().value) // => "woah there, send this back"
// how about the second yield of the second instance?
console.log(gen2.next().value) // => "also this, thanks!"
// what if want EVERY yield from our third instance next
console.log(gen3.next().value) // => "woah there, send this back"
console.log(gen3.next().value) // => "also this, thanks!"
console.log(gen3.next().value) // => "don't forget this!"
// let's grab the last yield of the first instance
console.log(gen.next().value) // => "also this, thanks!"

We can now see why our previous iteration of this code sent back our first yield three times- every time we called our generator function, it created a completely new instance and called it, but that instance was then immediately thrown away. We didn’t turn all of it’s metaphorical pages! We picked up The Odyssey, looked at one page, threw it away, picked up another copy of The Odyssey, looked at one page, threw that copy away, and then we picked up another one-

But in this new code, we’re saving each instance, we’re flipping through all the pages as we read, and we’re probably making all the people at the bookstore feel a lot less confused.

After we call our first instance and ask for the first value- we put it on pause. We look at our second instance and tell it to give us it’s first value. It pauses, but resumes when we ask for the second value- now we have a slightly deeper understanding of what’s going on under the hood. We just exited and re-entered our functions!

So far we’ve used:

  • the yield keyword to pause execution and give us values.
  • the next() method to resume execution of our functions.

We’ve received:

  • values accessed with .value (one of the two key/values on the entire object returned by generator functions).
  • the other key/value pair returned has a Boolean key called done, with a value of “true” or “false” depending on whether the generator is finished.

That leaves the last two methods for generator functions:

  • return(), which ends a generator early and returns a specified value.
  • throw(), which is used to throw errors.

Let’s get into it!

The return() method allows for a forced completion of the generator function before all yields are returned.

// we're creating an endless loop, intentionally for once!
function* endlessCount() {
let count = 1;
while (true) {
yield `I BROUGHT YOU A NUMBER, IT'S ${count}`;
count++;
}
}

// here we set our instance to counter
const counter = endlessCount();

// let's call it a few times!
console.log(counter.next().value); // => "I BROUGHT YOU A NUMBER, IT'S 1"
console.log(counter.next().value); // => "I BROUGHT YOU A NUMBER, IT'S 2"
console.log(counter.next().value); // => "I BROUGHT YOU A NUMBER, IT'S 3"

// the generator is forced to stop with return,
// inside of object- we see that the value we return is our input string
// and the done Boolean has a value of true
// note: we did not use .done or .value so we recieve our full object
console.log(counter.return("STOP!")); // => { value: 'STOP!!', done: true }

// if we try to get our next value...
console.log(counter.next().value); // => undefined is returned
console.log(counter.next().done) // => and checking out our done Boolean returns true

As you can imagine- this was the least traumatic infinite loop my computer has ever encountered. Because of the nature of generator functions pausing and then resuming- we don’t get the iconic page crash that makes computers weep and programmers panic.

Our last method, the throw() method, can be paired nicely with a try…catch statement in order to catch specific errors that might otherwise be ignored by the computer. These errors display as errors in the console, which is incredibly useful:

// function needs to be declared with *
function* generator() {
// try is going to be attempting to run everything, if we get an exception, we go to catch
try {
// our various yields, to be sent back at different times
yield "nothing to see here";
// this one will never run
yield "running sooooo smoothly!";
// catch is going to give us back a message if something wrong happens
} catch (error) {
console.log(`ERROR in function generator! : ${error}`);
}
}

// instance of our generator function
const gen = generator();

// grab the first yield of the first instance
console.log(gen.next().value) // => "nothing to see here"
// now the first yield of the second instance... wait a moment! .throw() threw an error!
console.log(gen.throw("whoopsies!").value) // => "ERROR in function generator! : whoopsies!"

In more practical code- throw() can be used to raise exceptions inside the function when certain conditions are met. It’s function is very similar to, but should not be mistaken for the throw statement- which is widely available.

After seeing their syntax and methods- you might be wondering: why are generator functions a big deal? Alternatively, you might be wondering why you never hear more of them, particularly when it comes to JavaScript.

Why Use a Generator Function?

Generator functions initially saw heavy use in API requests, pairing with Promises and libraries (I’m going to make up a library called totallyRealLibrary for our Generator Function example) in order to create a better solution than the previous solution of AJAX and JQuery:

// AJAX example:

// using var to fit in with the ES5 syntax
var targetUserId = 1;

$.ajax({
url: 'https://JSONplaceholdertextImadeup.com/users/' + targetUserId,
success: function(user) {

$.ajax({
url: 'https://JSONplaceholdertextImadeup.com/posts?userId=' + user.id,
success: function(posts) {
console.log("Got posts!", posts);
},
error: function(err) {
console.error("Failed to fetch posts!", err);
}
});

},
error: function(err) {
console.error("Failed to fetch user!", err);
}
});

// Generator Function Example:

// generator functions can pause, imagine our
// fake library is checking for completed Promises
totallyRealLibrary(function* () {

// we pause until user data is fetched
const userResponse = yield fetch('https://JSONplaceholdertextImadeup.com/users/1');
const user = yield userResponse.json();

// we pause until posts are fetched
const postsResponse = yield fetch('https://JSONplaceholdertextImadeup.com/posts?userId=' + user.id);
const posts = yield postsResponse.json();

console.log("Got posts!", posts);
});

While we’ve moved past that era, it’s worth noting that the behavior of async/await (the tools of the next era) produce a similar output to what developers were achieving when combining generators and promises.

Even more relevant is that async functions and generator functions have also been combined in the form of the async function* (also called an async generator). These functions are capable of handling massive amounts of data by breaking things down into smaller, more manageable sizes- think of pagination for example (fetch this page, then this page, then this page-).

But the average generator function has its strengths too! They also excel at handling large datasets, are very memory efficient due to lazy evaluation (they only compute the next value exactly when you ask for it- think of my earlier infinite loop that was not-so-infinite in it’s impact), and the most interesting thought for me: they can be used to create custom iterables.

So not knowing what those were, I discovered quite on accident that you can use custom iterables in order to iterate over a tree:

// we're going down my family tree today, yipee!
const myFam = {
name: "Lee",
children: [
{
name: "Michael",
children: [{ name: "Claire" }, { name: "Grant" }, { name: "Peter" }]
},
{
name: "Kyle",
children: [{ name: "Jacob" }, { name: 'Anderson' }]
}
]
};

// let's use a generator
function* traverse(node) {
// yield the current person's name
yield node.name;
// if that person has children
if (node.children) {
// we use a for of loop to loop over the children
for (const child of node.children) {
// we pause and recurse down our tree to the next child
// yield* will let us return individual names and
// not an entire generator object- think of it as
// being similar to flatten with arrays
yield* traverse(child);
}
}
}

// now lets make myFam object iterable
// because I don't care if it's a tree-
// the heart wants what it wants

// myFam + [Symbol.iterator] is going to tell JavaScript:
// when we try to loop over myFam, use the following function
myFam[Symbol.iterator] = function* () {
// we're going to have an instance of traverse run
yield* traverse(this);
};

// and now, with our coding mischief managed:
// we can use for of loops over a tree
for (const person of myFam) {
console.log(person); // => "Lee"
} // "Michael"
// "Claire"
// "Grant"
// "Peter"
// "Kyle"
// "Jacob"
// "Anderson"

With my happy little tree being iterated over so smoothly, I can’t help but feel that Generator Functions have shown me a side of coding I had never seen before. If you told me before this post that functions could return multiple values? I would have told you that’s impossible. But now, I see the way generator functions works as fundamental to the evolution of not only my own growing knowledge, but to JavaScript itself.

--

--