CodeRigor

FREE LESSON

Functions, lexical scope and closures

Write callbacks with explicit returns and keep independent state in lexical closures.

A function is a value that can be called. A declaration gives it a name; an arrow expression can make a small callback inline. Parameters are local bindings created for a call. return sends a value to the caller, and reaching the end sends undefined. Keep the difference between doing work and returning its result visible when reading callbacks.

JavaScript
const twice = value => value * 2;
const forgotten = value => { value * 2; };
console.log(twice(4));
console.log(forgotten(4));

A braced arrow body is a block, so it needs return. To return an object literal from an expression arrow, wrap the literal in parentheses: value => ({value}). Default parameters apply to missing or undefined arguments; they do not automatically replace null. Treat defaults as part of the function's interface instead of scattering fallback expressions throughout its body.

One factory, independent closures
start / 4
Each call owns its own current binding; returned functions retain access.
One factory, independent closures
Each call owns its own current binding; returned functions retain access.

Lexical scope means that the placement of the function in the source determines which outer bindings it can access. A closure keeps access to those bindings after the outer call returns. It does not freeze their initial values. If the captured binding changes, the closure observes the changed value. Each invocation of a factory can create a different binding with the same source-level name.

JavaScript
function makeLabel(prefix) {
  let calls = 0;
  return function(name) {
    calls += 1;
    return `${prefix}:${name}:${calls}`;
  };
}
const first = makeLabel("A");
const second = makeLabel("B");
console.log(first("job"), first("job"), second("job"));

Private mutable state is useful when its ownership is clear. Putting one counter at module scope would couple all callers. Keeping it inside each makeCounter invocation gives the returned function its own state. Validate a proposed update before storing it. If an overflow or invalid step throws, the next valid call should still begin from the previous successful value.

Validate before committing state
start / 1
An invalid candidate must leave the previous count intact.
Validate before committing state
An invalid candidate must leave the previous count intact.

Callbacks also define an execution contract. For select, pass each original value and its index exactly once, in ascending order. Store the callback's result rather than invoking it again to decide whether it was truthy. A callback may record events, increment a count or throw; duplicate calls change observable behavior. A fresh result array can still contain the original object references, which is intentional here.

Practice by tracing two counters on paper and interleaving their calls. Next, write a predicate that records every (value,index) pair before returning true only for odd indices. Check both the selected values and the event sequence. These examples expose mistakes that a single happy-path return value would miss. The build asks for explicit results, independent closures and predictable failure behavior, all of which make later asynchronous callbacks easier to reason about.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Focus on parameters, return values, local scope, arrow functions and higher-order functions. Trace two independent closures after the reading.