The Bug Hiding in a Reused Closure
A closure over a variable that looks private can still leak into other branches of your code, if the same mutable state gets shared between them.
The last post's rule was: closing over a variable stays safe only if it never changes. Here's a case where that rule gets broken in a way that's easy to miss.
Building a Note Builder
Say you want to keep appending pieces onto an order note, one call at a time:
function noteBuilder(str) {
return function next(v) {
if (typeof v === 'string') {
str += v;
return next;
}
return str;
};
}Call it once, it works exactly as expected:
const base = noteBuilder('Order #42');
base(' - ship today');
base(); // "Order #42 - ship today"Where It Breaks
Now branch off the same starting point twice:
const base = noteBuilder('Order #42');
const toWarehouse = base(' - ship today');
const toCustomer = base(' - thanks for shopping with us');
toWarehouse(); // "Order #42 - ship today - thanks for shopping with us"That's wrong. toWarehouse picked up the customer message too, even though nothing ever called it with that text.
Why: One Variable, Two Branches
next always mutates and returns the exact same str, and always returns the exact same next function. Call base(...) twice, and both calls hand back references to that identical function, closing over that identical, still-mutating variable.
toWarehouse and toCustomer were never two independent branches. They were two names for the same closure, over the same variable, and every future call from either name kept mutating the one shared str underneath both of them.
A closure protects a variable from the outside world. It does nothing to protect it from itself.
The Fix: A New Closure Per Step, Never a Mutation
The fix isn't a clever guard, it's removing the mutation entirely. Instead of changing str in place, call noteBuilder again with the new combined value, and let it hand back a completely fresh closure:
function noteBuilder(str) {
return function next(v) {
if (typeof v === 'string') {
return noteBuilder(str + v);
}
return str;
};
}Now every branch gets its own private copy:
const base = noteBuilder('Order #42');
const toWarehouse = base(' - ship today');
const toCustomer = base(' - thanks for shopping with us');
toWarehouse(); // "Order #42 - ship today"
toCustomer(); // "Order #42 - thanks for shopping with us" ExpandTwo diagrams. In the first, toWarehouse and toCustomer both branch from base but both arrows point into one shared "str" box, so either name mutating it contaminates the other. In the second, toWarehouse and toCustomer each point to their own independent str box.
Why This One Is Worth Remembering
Nothing about the broken version looked dangerous. str was declared inside the function, never exported, never touched from outside. It looked exactly as private as a closed-over variable is supposed to look.
The danger wasn't visibility, it was reuse. Branching off the same closure twice, then mutating what it shares, produces a bug that only shows up once two branches actually get used together, which in a real app might be one request in a million. The fix costs nothing here: recursing into a fresh closure instead of mutating a shared one is often just as fast, and it removes the entire category of bug outright.
Keep reading