← JavaScript: language and application diagnosis
02 / 10 · 50 MIN

Bindings, functions and closures

Analyze captured bindings, parameters and receivers so later callbacks observe the intended state.

Binding, object and visible effect

const settings = { retries: 1 } creates a binding that cannot be reassigned. The settings.retries property can still change. This distinction matters when passing data to functions: a parameter receives a value, and when that value references an object, the function and caller may reach the same object.

A write to p.count may be visible to the caller. By contrast, p = { count: 0 } changes only the local parameter binding. That reassignment neither replaces the caller’s variable nor undoes earlier writes. When diagnosing unexpected effects, trace references before and after the call; the word const alone does not establish immutable data.

An inner block can shadow a name before initialization

let and const have block scope. If a block declares let mode, reading mode earlier in that same block does not automatically fall back to an outer mode. The name resolves to the inner, uninitialized binding. Reading it throws ReferenceError; even typeof mode is subject to that temporal dead zone.

This differs from a name that has not been declared in any scope, for which typeof can produce "undefined". When reviewing a failure introduced by a local declaration, first identify which declaration the name now resolves to. Moving a read or renaming a variable can change resolution; describing the problem merely as a variable lacking its final value misses the relevant distinction.

Default parameters and returned values

In function quota(value = 4), the initializer is used when the argument is omitted or undefined. An explicitly supplied null remains null; zero is not replaced either. A default value does not declare a required parameter type. Initializers are evaluated at call time and their scope is outside the function body.

That location explains why a let base declared inside the body does not provide the base read by a parameter initializer. Return syntax also matters: n => n * 2 returns its expression, whereas n => { n * 2; } ends without return and yields undefined. A callback may complete normally while still failing to deliver the value its caller expected.

Closures retain access to bindings

A closure associates a function with the lexical environment in which it was created. It should not be imagined as always taking a snapshot of every value. If several callbacks read the same var i, they read that binding when called. After a loop covering indices 0 through 2 finishes, i is 3 and all may observe 3.

A for loop with let i creates per-iteration bindings. This lets each callback retain its corresponding index. Another approach is a separate function invocation with its own parameter. Two counters created by distinct factory calls have independent environments, while repeated calls to the same closure may update the same retained counter.

Receiver: explicit binding and lexical this

Calling reporter.read() supplies reporter as the method’s this. Retaining only reporter.read and later invoking it as a plain function does not automatically retain that receiver. In module code, that call does not supply a replacement global object: the method may fail when reading this.count.

reporter.read.bind(reporter) creates a callback bound to the object. It still reads the current property at invocation; it does not clone count. An arrow instead uses this from the context where it was created. call or bind cannot replace that lexical this. An arrow created at module top level does not acquire reporter as receiver merely by being stored in a property of that object.

Guided reading: identity at preparation time

In buildReaders, each iteration has its own i and reads id into a local const. Because the contract restricts id to a string, the later assignment records[0].id = "Z" does not change the string already read. readers[0]() returns a new object with index 0 and id "A". Parentheses in () => ({ index: i, id }) make the object the returned expression.

If the code retained records[i] itself instead of the string, that object’s property could change before invocation. If the contract required the current id, the example would need to read current state deliberately. Capturing earlier is a behavioral choice, not an automatic improvement for every callback.

Diagnosis by read timing

For an integration with deferred actions, record three moments: callback creation, state change and invocation. Then identify the binding or receiver read at each point. This sequence separates a shared index, a mutable reference and a function called without its expected object. Adding a timer alone fixes none of those contracts.

To complete the analysis, justify whether the result should reflect preparation-time or current state. Then choose value capture, a per-iteration binding or a bound receiver accordingly. In assessments, the snippets define order and environment; the task is to select the interpretation or correction consistent with those facts.

function buildReaders(records) {
  const readers = [];
  for (let i = 0; i < records.length; i++) {
    const id = records[i].id;
    readers.push(() => ({ index: i, id }));
  }
  return readers;
}
const records = [{ id: "A" }, { id: "B" }];
const readers = buildReaders(records);
records[0].id = "Z";
const first = readers[0]();
IN PRACTICE

Assume an already validated list with string ids and no mutation during buildReaders. The final value is { index: 0, id: "A" }: the iteration index and the string read at that time were captured. The example does not claim that a captured object would be deeply copied.

Common pitfalls

Confusing const with immutability; expecting var to save a value per iteration; omitting return in an arrow block; assuming bind copies properties; using module this as an object receiver.

Related topics: Objects, identity and property boundaries · Arrays, collections and iteration · Promises, async and error propagation

Take this idea with you

For each callback, identify the binding it reads, the object it reaches and when it reads that value.

Create account

Reference: Grammar and types · ECMAScript 2026 (17th edition); browser APIs identified separately