Variables
Scope, lifetime, mutability, and initialization
Variables
A variable is a binding between a name, a value, and a region of code where both are visible. Most defects related to variables come from one of three mistakes: using a value before it is meaningful, mutating a value in a place readers do not expect, or letting a binding outlive its purpose.
Initialization
Initialize at declaration
Whenever the language allows, declare and initialize in the same statement. A "declared but not yet assigned" variable is a window in which a stale or undefined value can be observed.
// Bad
let total;
// ... 30 lines ...
total = computeTotal();
// Good
const total = computeTotal();Declare at the point of first use
The C-style convention of declaring all variables at the top of a function pre-dates modern scoping and serves no purpose in current languages. Declare each variable as close to its first use as possible — ideally on the same line.
The benefit is concrete: the reader sees the initialization, the type, and the usage all at once, without scrolling.
Each variable, one purpose
Do not reuse a binding for unrelated values:
// Bad — `result` carries three distinct meanings
let result = computeRaw();
result = normalize(result);
result = result + tax;
return result;
// Better — each name describes one thing
const raw = computeRaw();
const normalized = normalize(raw);
const total = normalized + tax;
return total;The compiler does not care; the next reader does.
Scope and Lifetime
Keep scope as narrow as possible
The cost of a variable is proportional to the amount of code in which it is visible, not the amount of code in which it is used. A variable visible to 200 lines forces every reader of those 200 lines to consider it.
Practical applications:
- Declare loop variables inside the loop.
- If a class field is only read in one method, make it a local in that method.
- Prefer block scope (
let,const) over function scope (var).
Minimize live span
Live span is the distance between a variable's first and last use. Keeping live span short — by introducing intermediate names, extracting helpers, or reordering statements — reduces the working memory the reader needs.
Distinguish persistence levels explicitly
Be deliberate about how long a value lives:
| Lifetime | Examples |
|---|---|
| Expression | function arguments, intermediate values |
| Block | loop counters, local computations |
| Function | accumulators, results being built |
| Object | instance fields |
| Process | module-level singletons, caches |
| Persistent | database rows, files, key-value stores |
Confusing these levels — for example, storing per-request state on a process-level singleton — is a frequent source of multi-tenant bugs.
Mutability
Default to immutable
Prefer const, final, readonly, val, or the language's strongest immutability primitive. Allow mutation only when it materially simplifies the code.
Benefits:
- The type system tells the reader what cannot change.
- Concurrency reasoning collapses: an immutable value is safe to share.
- Time-travel debugging and equality become trivial.
Prefer functional updates over in-place mutation
// In-place
items.push(newItem);
// Functional
const updated = [...items, newItem];In-place mutation is the right choice when the data structure is large and performance-sensitive, or when the mutation is local and never escapes. Reach for the functional form by default.
Be explicit about ownership
When passing a mutable collection between modules, the calling and called code must agree on who is allowed to modify it. Document this. If the agreement is fragile, return an immutable view (Object.freeze, Collections.unmodifiableList, persistent data structures).
Numbers
Replace magic numbers with named constants
// Bad
if (retries > 3) throw new Error('too many');
// Good
const MAX_RETRIES = 3;
if (retries > MAX_RETRIES) throw new Error('too many');The name documents the intent and centralizes the value. The exceptions are the few constants that need no explanation: 0, 1, -1, sometimes 2.
Floating-point comparisons
Never compare floats with ===. Use a tolerance appropriate to the scale of the values, or a library function like Math.isClose.
Beware integer overflow in arithmetic
The classic example is binary search:
const mid = (low + high) / 2; // can overflow
const mid = low + (high - low) / 2; // safeIn typed languages, prefer the widest sensible numeric type for accumulators.
Booleans
Name intermediate booleans
Compound conditions are easier to read when their parts are named:
// Hard to scan
if (date < SUMMER_START || date > SUMMER_END || isHoliday(date)) { ... }
// Self-documenting
const isOffSeason = date < SUMMER_START || date > SUMMER_END;
const isClosed = isOffSeason || isHoliday(date);
if (isClosed) { ... }Avoid double negatives
if (!isNotReady) requires two passes to parse. Pick the affirmative form (isReady) and stick with it.
Strings
- Distinguish user-facing text (subject to internationalization) from internal identifiers (stable strings used in code paths).
- Never construct SQL, shell commands, HTML, or other interpreted languages by string concatenation. Use parameterized APIs or established escaping libraries — the alternative is an injection vulnerability.
- For complex string assembly, prefer template literals or a builder over
+chains.
Collections
Choose the structure that matches the semantics
| If you need… | Use |
|---|---|
| Unique membership | Set |
| Key → value lookup | Map |
| Ordered, indexable | Array / List |
| FIFO or LIFO | Queue / Deque |
| Sorted by key | TreeMap / SortedSet |
Reaching for a List and writing manual deduplication or linear search is a sign of the wrong choice.
Return read-only views when crossing boundaries
When a method returns a collection that the caller should not modify, return a read-only view. Internal mutation by external callers is a frequent source of "spooky action at a distance."
Pre-Commit Checklist
- Is every variable initialized at declaration?
- Is every variable declared close to its first use?
- Does each variable hold one consistent meaning throughout its life?
- Is the scope as narrow as the code allows?
- Are mutable bindings the exception, not the default?
- Have magic numbers been replaced with named constants?
- Does each collection match the operations performed on it?