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, always
A 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 untouched
Immutability 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

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.