DEV Community

Cover image for JavaScript Isn’t Broken — Your Closure Placement Is
Priya Pandey
Priya Pandey

Posted on

JavaScript Isn’t Broken — Your Closure Placement Is

Now and then, JavaScript quietly teaches me something, sitting under the surface. Recently, that lesson came from a tiny custom iterator I was writing just out of curiosity.

I wasn't trying to solve anything deep.
But while experimenting, I bumped into a pattern that shows up in so many places:

Where you put your closure state determines whether your function/object becomes reusable or single-use.

It's a deeper concept that affects half the JavaScript patterns we write without even thinking.

Let me show you how I discovered it.


Example 1: A Custom range() Iterator — Not Wrong, Just Interesting

Here's the custom range() iterator I wrote:

function range(from, to) {
  let nextIndex = from;  // this state lives in the closure of range()

  return {
    next() {
      if (nextIndex <= to) {
        return { value: nextIndex++, done: false };
      }
      return { done: true };
    },
    [Symbol.iterator]() {
      return this;  // this object IS the iterator
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Using it:

const r = range(1, 4);

for (let x of r) console.log(x);
// -> 1 2 3 4

for (let x of r) console.log(x);
// -> (nothing)
Enter fullscreen mode Exit fullscreen mode

And here's the thing:

  • This is not a bug.
  • This is exactly how generator functions behave.

Generators are intentionally single-use.
Once their internal state moves forward, it stays moved.
My custom iterator behaves the same way.

So nothing is "wrong".

But this led me to a deeper realization:

The single-use behavior has nothing to do with iterators being tricky.

It's simply because:

  • The state (nextIndex) lives in the outer closure
  • And [Symbol.iterator] returns the same object every time

Which means the iterator cannot restart - the internal state never resets.

This is where the real pattern shows up.


If You Want a Reusable Iterable…

…you only need to change where the state lives:

function range(from, to) {
  return {
    [Symbol.iterator]() {
      let nextIndex = from;  // fresh state per iterator instance
      return {
        next() {
          if (nextIndex <= to) {
            return { value: nextIndex++, done: false };
          }
          return { done: true };
        },
      };
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Now:

for (let x of r) console.log(x); // 1 2 3 4
for (let x of r) console.log(x); // 1 2 3 4   (restarts)
Enter fullscreen mode Exit fullscreen mode

Fresh iterator -> fresh nextIndex -> reusable iterable.

And that's when I realized:

This isn't about iterators.
It's about closure state and how we structure our functions.

To test the theory, I tried something completely different - a pipe() function.

And it exposed the same pattern.


Example 2: A Pipe Function That Accidentally Became Single-Use

If you haven't used pipe() before:
It's a small functional helper that takes a list of functions and runs them in sequence:

pipe([f1, f2, f3])(value)
Enter fullscreen mode Exit fullscreen mode

is basically:

f3(f2(f1(value)))
Enter fullscreen mode Exit fullscreen mode

Here's the first version I wrote:

function pipe(funcs) {
  let i = 0; // state stored OUTSIDE merged()

  return function merged(...args) {
    if (funcs.length === 0) return args[0];

    const out = funcs[i](...args);

    if (i === funcs.length - 1) {
      return out;
    }

    i++;
    return merged(out);
  };
}

Enter fullscreen mode Exit fullscreen mode

Usage:

const fn = pipe([x => x * 2, x => x + 3]);

console.log(fn(5));  // 13
console.log(fn(5));  // 8 (state leaked across calls)
Enter fullscreen mode Exit fullscreen mode

And then I laughed because this is the same pattern as the iterator example:

  • 'i' lives in the outer closure
  • The returned function shares that 'i' across invocations
  • Which means the whole pipeline is single-use

Exactly like the iterator.


Fixing It (Again) By Moving State Inside

function pipe(funcs) {
  return function merged(...args) {
    function helper(i, val) {
      if (i >= funcs.length) return val;
      return helper(i + 1, funcs[i](val));
    }
    return helper(0, ...args);  // fresh state per call
  };
}
Enter fullscreen mode Exit fullscreen mode

Now:

fn(5); // 13
fn(5); // 13

Enter fullscreen mode Exit fullscreen mode

The Actual Pattern

After seeing this in both examples, the pattern became very clear:

If your state lives in the outer closure, you create a single-use function/object.
If your state is created inside a nested function or factory, you get a reusable function/object.

This applies to:

  • iterators
  • generators
  • pipes
  • debounce/throttle
  • decorators
  • memoization
  • middleware
  • event callback wrappers

Basically, half of JavaScript.


What This Taught Me

I used to look at nested functions and think:

"Why do people put functions inside functions? Just flatten it!"

Now I understand why:

  • Nested functions give you fresh closure state
  • Fresh state gives you reusable behavior
  • Outer closure state gives you single-use behavior

What About You?

Have you ever written something that mysteriously worked only once?
Or something that behaved "weirdly" because of closure state placement?

I'd love to hear other examples - this pattern is everywhere once you learn to see it.

Top comments (5)

Collapse
 
dariomannu profile image
Dario Mannu

As you continue "evolving" this work (well done, btw), you end up rediscovering something like IxJS or something even more powerful and useful such as Observables. RxJS will open up a whole world of possibilities once you discover it, let alone if you combine it with Rimmel.js for the UI...

Collapse
 
art_light profile image
Art light

Wow, this was a super clear explanation — I really enjoyed how you broke down the closure pattern with simple examples. Great insight, and it actually made me rethink how I structure functions!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.