Node.js Generators & Compare with Callbacks
โก Smart Summary
Generators in Node.js are functions whose execution can be suspended and resumed on demand. The yield keyword pauses the function, the next method resumes it, and that pairing flattens deeply nested callbacks into readable sequential code.

In this walkthrough we look at generators and how they differ from callbacks.
What are generators?
Generators became popular in Node.js because of what they are capable of doing.
- Generators are function executions that can be suspended and resumed at a later point.
- Generators are useful for concepts such as โlazy executionโ. By suspending execution and resuming at will, values are pulled only when they are needed.
Generators have the two key methods below.
- Yield method โ called inside a function to halt execution at the exact line where
yieldappears. - Next method โ called from the main application to resume a function that has yielded. Execution continues until the next
yield, or until the end of the function.
Let us look at an example of how generators can be used. The example defines a simple Add function that adds two numbers, halting execution at different points to show how generators behave.
function* Add(x) { yield x + 1; // The value passed into the second next() call lands here var y = yield(null); return x + y; } var gen = Add(5); gen.next(); // runs to the first yield, returns { value: 6, done: false } gen.next(6); // resumes, y becomes 6, returns { value: 11, done: true }
Code Explanation
- The first step is to define the generator function. Note the
*added to thefunctionkeyword. The function is calledAddand takes a parameterx. - The
yieldkeyword is specific to generators and pauses the function mid-execution. Execution halts here untilnext()is invoked. - The second
yieldboth pauses the function and receives a value: whatever is passed to the followingnext()call becomes the value ofy. - Calling
gen.next()the first time runs the function up to the firstyield. Callinggen.next(6)resumes it, assigns 6 toy, and returnsx + y.
โ ๏ธ Correction: the original listing contained the line y = 6 immediately after var y = yield(null);. That assignment overwrites whatever value next() supplies, which defeats the purpose of the example and makes the second yield meaningless. The value is now passed in through gen.next(6), which is how a generator actually receives data from the caller.
Callbacks vs generators
Generators solve the problem known as callback hell. Callback functions sometimes become so deeply nested during development of a Node.js application that they are difficult to follow.
This is where generators help. One of the most common examples is creating timer functions. The example below builds a simple time delay function and then calls it with delays of 1000, 2000, and 3000 ms.
Step 1) Define the callback function with the necessary time delay code.
function Timedelay(ptime, callback) { setTimeout(function() { callback("Pausing for " + ptime); }, ptime); // the delay must use ptime, the parameter name }
Code Explanation
- A function called
Timedelayis created with a parameterptime, which carries the number of milliseconds to pause for. setTimeoutwaits for that many milliseconds and then invokes the supplied callback.
โ ๏ธ Correction: the original code passed time as the second argument to setTimeout, but no variable named time exists โ the parameter is ptime. As written it throws ReferenceError: time is not defined.
Step 2) Now consider the code using callbacks. To chain delays of 1000, 2000, and 3000 milliseconds, the callbacks nest inside one another.
Timedelay(1000, function(message) { console.log(message); Timedelay(2000, function(message) { console.log(message); Timedelay(3000, function(message) { console.log(message); }); }); });
Code Explanation
Timedelayis called as a callback with 1000 as the value.- Inside that callback,
Timedelayis called again with 2000. - Inside that one,
Timedelayis called a third time with 3000.
โ ๏ธ Correction: each callback in the original logged msg, but the parameter is named message. All three lines threw ReferenceError: msg is not defined.
From the code above you can see it becomes messier with each additional call โ this nesting is what callback hell describes.
Step 3) Now see the same behaviour written with generators. The sequence reads as three flat lines rather than three levels of nesting.
function* Messages() { console.log(yield Timedelay(1000)); console.log(yield Timedelay(2000)); console.log(yield Timedelay(3000)); }
Code Explanation
- A generator function is defined to call the
Timedelayfunction. yieldis used withTimedelayand 1000 as the parameter value.yieldis used again with 2000, and once more with 3000.
โ ๏ธ Correction: the original wrote console,log(...) with a comma instead of a dot on all three lines, which is a syntax error and prevents the file from parsing at all.
How to Run a Generator with a Runner Function
The generator above reads beautifully but will not do anything on its own. This is the detail most explanations omit, and it is the reason the flat version can look like magic. A generator is a paused machine, and something has to keep pressing the button. Calling Messages() merely creates the generator object and returns immediately; no timer ever starts and nothing is printed.
The missing piece is a runner: a small function that calls next(), waits for the asynchronous work to finish, and then calls next() again with the result.
// Timedelay now hands its result back to the runner function Timedelay(ptime) { return function(resume) { setTimeout(function() { resume("Pausing for " + ptime); }, ptime); }; } // The runner drives the generator to completion function run(generatorFunction) { var it = generatorFunction(); function step(value) { var result = it.next(value); if (result.done) return; // result.value is the function returned by Timedelay result.value(step); } step(); } run(Messages);
Three points make this work. First, Timedelay no longer takes a callback directly; it returns a function that the runner can invoke, handing back control when the timer fires. Second, it.next(value) feeds the previous result into the paused generator, which is what allows yield to appear on the right side of an assignment. Third, the runner stops as soon as result.done becomes true, so the recursion terminates naturally. This runner pattern is exactly what libraries such as co provided, and it is the mechanism that async/await later absorbed into the language itself.
Generators vs async/await: Which to Use
Generators were the standard way to write flat asynchronous code before async/await existed, and the two share the same underlying idea of pausing a function partway through. Node has supported async/await natively since version 7.6, which changes the recommendation for anyone starting new work today.
| Aspect | Generators | async/await |
|---|---|---|
| Runner needed | Yes, written by you or a library | No, built into the language |
| Pause keyword | yield |
await |
| Error handling | Runner must forward errors | Ordinary try and catch |
| Can pause partway | Yes, and can be resumed selectively | No, runs to completion |
| Best for | Lazy sequences and infinite streams | Ordinary asynchronous work |
Use async/await for asynchronous flow in new code, since it removes the runner and restores ordinary try and catch. Keep generators for what only they do well: producing values lazily, iterating large or infinite sequences, and any case where the consumer decides exactly when the next value is computed.
Common Generator Errors and How to Fix Them
Most generator problems come from forgetting that the function is inert until something calls next(), or from small syntax slips around the asterisk. Each symptom below names its cause and its fix.
- Nothing happens when the generator is called: calling it returns an iterator, not a result. Call
next(), or pass it to a runner. - SyntaxError on yield: the asterisk is missing.
yieldis only valid inside afunction*. - The value from next() arrives undefined: the first
next()call cannot deliver a value, because the generator has not reached ayieldyet. Send data from the second call onward. - Infinite loop in a runner:
result.doneis never checked, sonext()keeps being called past the end. Always return when done is true. - Errors vanish silently: a rejection inside the asynchronous step is never passed back into the generator. Forward it with
it.throw(err)so that a try and catch written inside the generator can actually see and handle it.




