mutable bindings that drift out from under you
var / let / mut Declarations
Reassignable variables and secretly-shared defaults let state change between reads, so the value you assume is rarely the value you hold.
A binding you can reassign is a binding the next reader cannot trust.
01in the wild
In the wild
Reassignment You Can't Trust
A binding you can reassign can change between the moment a callback captures it and the moment it runs.
example.js
// SMELL: `let` can be rebound after a callback captures it
let config = loadConfig();
schedule(() => save(config)); // captures the BINDING, not the value
config = reloadConfig(); // the callback will now see this one
// RIGHT: a fresh const per value; nothing can rebind it
const config = loadConfig();
schedule(() => save(config)); // this exact config, alwaysA closure captures the variable, not a snapshot. Reassigning let later silently changes what the callback sees.
// observed
let: callback saves the reloaded config const: callback saves the config it was given
example.rs
// Rust makes mutation opt-in and visible.
let x = 5;
// x = 6; // compile error: cannot assign twice
let mut y = 5; // `mut` is a loud, searchable marker
y = 6; // allowed -- and obvious in review
// Prefer shadowing to transform without mutating:
let total = 5;
let total = total + 1; // new binding, old one untouchedImmutability is the default; mut is a keyword you must type and a reviewer can grep for.
// observed
without mut: reassignment refused at compile time with mut: permitted, but explicit and auditable
var Hoisting & Scope Leak
var ignores block scope and hoists to the top of the function, so a name leaks past where you declared it.
example.js
// SMELL: var is hoisted and function-scoped
function lookup(list) {
for (var i = 0; i < list.length; i++) {
if (list[i] === null) break;
}
return i; // <-- i still visible here; leaks the loop counter
}
// RIGHT: let is block-scoped; the name cannot escape
function lookup(list) {
let found = -1;
for (let i = 0; i < list.length; i++) {
if (list[i] === null) { found = i; break; }
}
return found;
}var hoists to the function top, so i outlives the loop. let keeps the binding inside the block where it belongs.
// observed
var: returns the leaked counter, even after the loop let: i does not exist outside the loop
example.py
# Python has no block scope either: the loop var leaks
for row in rows:
if row.done:
break
print(row) # <-- 'row' is whatever the loop left behind
# RIGHT: capture what you mean explicitly
last = next((r for r in rows if r.done), None)
print(last)After a for-loop the loop variable keeps its final value. Relying on that leaked binding hides intent.
// observed
leak: prints the last-seen row, not necessarily a 'done' one right: 'last' is exactly the row you searched for
02cross-pollination
Where this compounds
Nondeterminism
- Out-of-Order Async Completion × Async IO Misuse
- Stale Read From a Lagging Async Replica × Async IO Misuse
Runtime Errors
- Divide-by-Zero from an Empty Aggregate × Divide by Zero
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.