Javascript Generator Functions

Every Redux Saga handler is a generator function. Understanding how pause-and-resume works in JavaScript is the prerequisite that makes Saga code actually readable.

June 28, 20264 min read1 / 2

I spent time staring at function* and yield in a Saga handler, then gave up and looked up generators separately. That detour was necessary. Where Redux Thunk dispatches functions and Redux Promise detects promise-shaped payloads, Redux Saga builds on a JavaScript primitive that lets a function pause mid-execution and hand control back to the caller. That primitive is the generator function.

[!TIP] Run this yourself: All code examples are in the code-practice repo. Clone it and run node generators.js.

What Makes a Function a Generator

A single * after the function keyword is the only syntactic change. The behavior difference is dramatic.

JavaScript
function doWork() { console.log('started'); } doWork(); // logs "started" immediately function* doWork() { console.log('started'); } doWork(); // logs nothing

Calling a regular function runs its body top-to-bottom and returns. Calling a generator function does neither. The body does not execute.

Calling a Generator Returns an Object

The return value of calling a generator is a generator object. It holds the paused state of the function and exposes one method: .next(). Only when you call .next() does the body start executing.

JavaScript
function* doWork() { console.log('do work invoked'); } const gen = doWork(); console.log(gen); // GeneratorObject { ... } gen.next(); // "do work invoked" -- body runs now

The generator object is the handle. .next() is what drives execution.

yield: Return and Pause

Inside a generator, yield does two things at once: it returns a value to the caller and pauses the function at that exact line. Local variables stay intact. The function picks up from the same line on the next .next() call.

JavaScript
function* doWork() { console.log('step 1'); yield 1; console.log('step 2'); yield 2; } const gen = doWork(); console.log(gen.next()); // "step 1" { value: 1, done: false } console.log(gen.next()); // "step 2" { value: 2, done: false }

.next() always returns an object with two properties:

  • value: the yielded value
  • done: whether the function has finished

done: false means the generator is paused, not finished. Each .next() resumes it from exactly where it stopped.

The done Flag and return

yield keeps done: false because more code remains. return sets done: true because the function is complete.

JavaScript
function* doWork() { yield 1; yield 2; return 'work done'; } const gen = doWork(); gen.next(); // { value: 1, done: false } gen.next(); // { value: 2, done: false } gen.next(); // { value: 'work done', done: true } gen.next(); // { value: undefined, done: true } -- already finished

Once done: true appears, the generator is exhausted. Any further .next() calls return { value: undefined, done: true }.

A Real Example: Stepwise Addition

The practical case is a large task split into steps where the caller wants an intermediate result after each one. A caller that needs the next result calls .next() again; one that is done simply stops.

JavaScript
function* addInSteps(a, b, c) { let sum = 0; sum += a; yield sum; // 100 sum += b; yield sum; // 300 sum += c; yield sum; // 600 } const gen = addInSteps(100, 200, 300); gen.next(); // { value: 100, done: false } gen.next(); // { value: 300, done: false } gen.next(); // { value: 600, done: false } gen.next(); // { value: undefined, done: true }

The sum variable persists across every .next() call. The generator remembers its local state between resumes.

Generator pause-and-resume cycle: each .next() call resumes execution until the next yield returns a value and pauses again ExpandGenerator pause-and-resume cycle: each .next() call resumes execution until the next yield returns a value and pauses again

Why Redux Saga Uses This

Redux Saga wraps this mechanism to manage async side effects. Instead of yielding numbers, a saga yields effect descriptors, plain objects that describe what should happen next: call this function, wait for this action, put this value into state. The Saga middleware reads those descriptors, executes them, and resumes the generator with the result.

The generator never calls the API directly. It describes intent. The middleware handles execution. That separation is what makes Saga handlers both predictable and easy to test. The next post covers what problem this execution model solves -- what Redux Saga actually is and why it is built on generators.

The Essentials

  1. Calling a generator function does not execute its body. You get a generator object back. Execution starts with the first .next() call.
  2. yield returns a value and pauses. Local variables are preserved. The next .next() call resumes from the line after the yield.
  3. .next() always returns { value, done }. done: false means more yields are coming. done: true means the function has finished.
  4. return inside a generator sets done: true. Additional .next() calls after that return { value: undefined, done: true }.
  5. Redux Saga builds directly on this. Sagas yield effect descriptors instead of data values. The middleware executes those effects and feeds the results back in, keeping the generator as the control-flow layer.

Further Reading and Watching

Practice what you just read.

Generator Functions -- Quiz
1 exercise