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.

  • โธ๏ธ Core Behaviour: A generator suspends at each yield and resumes exactly where it stopped when next is called.
  • โœณ๏ธ Declaration: An asterisk after the function keyword marks the function as a generator rather than an ordinary one.
  • ๐Ÿ”„ Two-Way Channel: A value passed into next becomes the result of the yield expression that paused the function.
  • ๐Ÿช† Callback Hell: Nested callbacks grow unreadable as steps are added, which is the problem generators were adopted to solve.
  • ๐Ÿƒ Runner Required: A generator does not drive itself; a runner function must call next repeatedly for asynchronous work to proceed.
  • ๐Ÿฆฅ Lazy Execution: Suspending and resuming lets a program pull values only at the moment they are needed.

Node.js Generators Compared with Callbacks

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.

  1. Yield method โ€” called inside a function to halt execution at the exact line where yield appears.
  2. 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.

Node.js Generators

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

  1. The first step is to define the generator function. Note the * added to the function keyword. The function is called Add and takes a parameter x.
  2. The yield keyword is specific to generators and pauses the function mid-execution. Execution halts here until next() is invoked.
  3. The second yield both pauses the function and receives a value: whatever is passed to the following next() call becomes the value of y.
  4. Calling gen.next() the first time runs the function up to the first yield. Calling gen.next(6) resumes it, assigns 6 to y, and returns x + 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.

Callbacks vs. Generators

function Timedelay(ptime, callback) {

    setTimeout(function() {

        callback("Pausing for " + ptime);

    }, ptime);   // the delay must use ptime, the parameter name
}

Code Explanation

  1. A function called Timedelay is created with a parameter ptime, which carries the number of milliseconds to pause for.
  2. setTimeout waits 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.

Callbacks vs. Generators

Timedelay(1000, function(message) {

    console.log(message);

    Timedelay(2000, function(message) {

        console.log(message);

        Timedelay(3000, function(message) {

            console.log(message);
        });
    });
});

Code Explanation

  1. Timedelay is called as a callback with 1000 as the value.
  2. Inside that callback, Timedelay is called again with 2000.
  3. Inside that one, Timedelay is 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.

Callbacks vs. Generators

function* Messages() {
    console.log(yield Timedelay(1000));
    console.log(yield Timedelay(2000));
    console.log(yield Timedelay(3000));
}

Code Explanation

  1. A generator function is defined to call the Timedelay function.
  2. yield is used with Timedelay and 1000 as the parameter value.
  3. yield is 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. yield is only valid inside a function*.
  • The value from next() arrives undefined: the first next() call cannot deliver a value, because the generator has not reached a yield yet. Send data from the second call onward.
  • Infinite loop in a runner: result.done is never checked, so next() 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.

FAQs

An object with two properties: value, holding whatever was yielded, and done, a boolean that turns true once the generator finishes. Runners test done to know when to stop.

Yes. A generator containing an endless loop is safe because nothing computes until next() is called. This makes generators well suited to streams of identifiers or paginated results.

Yes. AI tools swap yield for await and drop the runner entirely. Review lazy sequences carefully, because a generator that never finishes has no direct async await equivalent.

AI assistants trace whether the runner stopped calling next, whether a resume callback was never invoked, or whether done is never reached. Those three account for nearly every stalled generator.

No. An async generator is declared async function* and is consumed with for await...of. It combines both ideas and suits streaming asynchronous data such as database cursors.

Summarize this post with: