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.
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.
function doWork() {
console.log('started');
}
doWork(); // logs "started" immediately
function* doWork() {
console.log('started');
}
doWork(); // logs nothingCalling 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.
function* doWork() {
console.log('do work invoked');
}
const gen = doWork();
console.log(gen); // GeneratorObject { ... }
gen.next(); // "do work invoked" -- body runs nowThe 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.
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 valuedone: 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.
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 finishedOnce 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.
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.
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
- Calling a generator function does not execute its body. You get a generator object back. Execution starts with the first
.next()call. yieldreturns a value and pauses. Local variables are preserved. The next.next()call resumes from the line after theyield..next()always returns{ value, done }.done: falsemeans more yields are coming.done: truemeans the function has finished.returninside a generator setsdone: true. Additional.next()calls after that return{ value: undefined, done: true }.- 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
- Generators in JavaScript - What, Why and How (Fun Fun Function): the clearest walkthrough of the generator mental model and how iterators plug into the same protocol
- MDN: function*: full language reference covering infinite generators and the iterator protocol
Practice what you just read.