Course outline

JavaScript / Basic: values and browser foundations / Lesson 3

Functions, parameters, and lexical scope

Design small functions with visible inputs and outputs. Trace scope and return behavior using reusable pricing and eligibility rules rather than relying on hidden global state.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define clear function contracts and return paths.
  • Trace lexical scope and block bindings.
  • Distinguish declaration, expression, and callback use.

A function owns a small transformation

Treat a function as an explicit relationship between inputs and a result or effect. A total calculator accepting integer cents and a valid quantity can return a number without reading a form, changing the page, or logging private data. The caller handles parsing and display. This separation lets a single rule be used in a browser, a test, or another JavaScript host. Document assumptions such as nonnegative prices and maximum quantity; a short function is not automatically a safe public boundary. Decide whether invalid input throws, returns a structured error, or is forbidden by its internal contract.

javascript Explicit arguments and results
function totalCents(unitCents, quantity) {
  return unitCents * quantity;
}
console.log(totalCents(250, 3));
console.log(totalCents(250, 0));
Expected output
750
0

The calculation does not read UI state. Zero is a meaningful arithmetic case, even if an enrollment boundary separately requires a positive quantity.

Trace every return path

A function that reaches its end without returning a value returns undefined. A return statement ends that invocation, so code after it on the same path is unreachable. Arrow functions with an expression body return that expression; braces introduce a block and usually require an explicit return. Returning an object literal from a concise arrow requires parentheses so braces are not parsed as a block. Keep the distinction visible in collection callbacks: map needs a returned value for each element, whereas forEach is chosen for effects and does not construct the transformed array.

Scope follows where code is written

Lexical scope resolves a name through the surrounding scopes where a function was defined, not through whichever caller happens to invoke it. Function parameters and local bindings prevent unrelated callers from overwriting one another’s work. let and const are block scoped, and accessing them before initialization in their scope produces a ReferenceError rather than a useful initial value. Avoid relying on declarations being moved mentally to the top. Declare bindings near the point their value becomes available and choose distinct names when shadowing would obscure which value a rule uses.

Pass behavior without running it early

A callback is a function supplied to another operation. Passing calculate gives the function value; passing calculate() supplies its result immediately. The receiving API decides when, with which arguments, and how often a callback runs. Read that contract rather than assuming every callback is asynchronous. Array map invokes its callback during the current call, while a browser event listener waits for a later dispatch. An arrow captures this lexically, unlike an ordinary method call’s receiver-dependent this. Avoid changing function form casually when receiver behavior is part of the design.

javascript A synchronous callback with lexical lookup
const rate = 5;
function addFee(amount) { return amount + rate; }
function caller() {
  const rate = 100;
  return [10, 20].map(addFee);
}
console.log(JSON.stringify(caller()));
Expected output
[15,25]

addFee resolves the outer five, not caller’s local hundred. map passes each amount synchronously and collects the returned values.

Keep effects at a deliberate boundary

Pure calculations are easier to test because equal inputs produce equal results without changing outside state. Real applications still need effects such as rendering and requesting data; keep those in small coordinating functions instead of pretending they disappear. A useful decomposition is parse, validate, calculate, then display. Each stage has a different failure meaning. Do not hide a failed validation by returning the same zero used for a valid free item. Give results distinct representations so callers can choose a correct message and avoid performing an effect after a rejected calculation.

Common errors and fixes

map produces undefined entries

Why it happens
A block-bodied arrow omits return.
How to fix it
Return the intended expression or use a concise body.
Prevent it next time
Assert the actual transformed values, not only array length.

A handler runs during registration

Why it happens
The caller passes handler() instead of the function.
How to fix it
Pass the function or a wrapper that supplies arguments later.
Prevent it next time
Check callback timing in the receiving API.

A valid zero conceals an invalid input

Why it happens
Error and success share an ambiguous return convention.
How to fix it
Use a distinct error or structured result.
Prevent it next time
Document all return paths and test rejection separately.

Check your understanding

1. Repair items.map(x => { x * 2; }).

Show worked solution

Use items.map(x => x * 2) or return x * 2 inside the braces. For [2, 4], expect [4, 8]. The original callback executes multiplication but returns undefined, so checking only that the callback ran would miss the defect.

2. A calculator reads a global discount. How can its contract improve?

Show worked solution

Pass discount as a validated explicit parameter and return the calculated amount. The caller chooses the current discount and owns any display effect. Test the function with zero and a nonzero discount without changing global state between cases.

Key takeaways

  • Names resolve lexically; callbacks do not imply asynchronous work.
  • A clear return contract separates valid results from rejection.

Official references