Module 1 · Foundations & First Steps ⏱ 18 min

Functions & Arrow Functions

By the end of this lesson you will be able to:
  • Define functions with the function keyword and with arrow syntax
  • Return values and pass arguments, using the words parameter and argument precisely
  • Explain how scope isolates a function's names from the rest of the program
  • Avoid the classic var-in-a-loop closure trap and read hoisting correctly

So far your code has run straight down the page, once. That works until you need the same logic in three places — so you copy it, fix a bug in one copy, and forget the other two.

A function is a named, reusable block. You define it once and call it as often as you like. But functions buy you a second thing that matters even more: a private workspace called scope. Names you create inside a function live there and nowhere else, so a function cannot quietly overwrite the variables in the rest of your program. Most beginner 'why did my variable change?' bugs are really scope bugs, so we treat scope as the heart of this lesson, not an afterthought.

Two ways to write the same function:

A function is a recipe you define once and cook many times. Press Run.
function double(n) {
  return n * 2;
}

const triple = (n) => n * 3;   // arrow, short form: returns n * 3 automatically

// Defining printed nothing. Calling does the work.
console.log(double(5));
console.log(triple(5));
console.log(double(double(3)));
flowchart LR
  A["double(5)"] --> F["function body: n * 2"]
  F --> R["returns 10"]
  style F fill:#e0900b,color:#fff
A function receives arguments, runs its body, and returns a value.

Parameters, arguments, and return

Two words get used loosely but mean different things. In function double(n), the n is a parameter — a placeholder name in the definition. The 5 in double(5) is an argument — the actual value you hand over. Keeping those straight makes later error messages readable.

The body runs only when the function is called, never when it is defined. Whatever follows return is handed back to the caller; the function stops there. A function with no return still gives something back — undefined — which is the source of a classic bug: it printed the right answer, so it looked correct, but the value it actually returned was undefined. Anything that computes should return; reach for console.log only at the outermost layer that talks to a human.

An arrow function with a single expression returns that expression automatically — no return, no braces. Add braces and you must write return yourself.

Functions shine as arguments to array methods like map and reduce.
const nums = [1, 2, 3, 4];
const doubled = nums.map((n) => n * 2);
console.log(doubled);

const total = nums.reduce((sum, n) => sum + n, 0);
console.log("sum =", total);

Scope: a function's names are its own

When a function runs, it gets a private workspace. A const or let declared inside the body exists only for the duration of the call and is thrown away when it ends. That is what stops a function from clobbering your program's state: if a global total exists and your function declares its own total, the local one shadows the global for the call — the global is untouched.

Reading is more permissive than writing. If a name is not defined locally, JavaScript walks outward along the scope chain to find it, so an inner function can read an outer variable. The reverse is not true: the outer scope cannot see names declared inside a function. The safe habit that follows from this is simple — take what you need as parameters, hand results back with return.

flowchart LR
  G["global scope"] --> M["function scope"] --> I["nested block scope"]
  I -.->|"not found locally?<br/>walk outward"| M
  M -.->|"still not found?"| G
  style G fill:#3776ab,color:#fff
  style M fill:#b45309,color:#fff
  style I fill:#e0900b,color:#fff
The scope chain: look locally first, then walk outward. Outer scopes cannot see in.
Shadowing protects the outer name; a closure remembers an outer one.
const name = "Ada";            // global
function greet() {
  const name = "Grace";        // local shadows the global
  return `Hi, ${name}`;
}
console.log(greet());          // "Hi, Grace"
console.log(name);             // "Ada" — the global was never touched

// A closure: the inner function remembers `base` after makeAdder returns.
function makeAdder(base) {
  return (n) => base + n;
}
const addTen = makeAdder(10);
console.log(addTen(5));        // 15

Closures: a function that remembers

When makeAdder returns, its parameter base ought to disappear with it. It does not — the returned arrow closes over base and keeps it alive. That captured binding is a closure, and it is how a function can carry private state around with it. Each call to makeAdder(10) and makeAdder(100) builds a separate, independent base; the two closures do not share one.

Closures are powerful, but they have one famous trap, and it comes from the older var keyword. let and const are block-scoped — a new copy per { ... } block, including each trip through a for loop. var is function-scoped, so a single var i is shared across every iteration of a loop. Close over that shared var and every callback ends up seeing the same final value.

flowchart TD
  D["function f() {...}<br/>declaration"] --> H1["hoisted: callable above its line"]
  E["const f = () => {...}<br/>expression"] --> H2["not hoisted: dead zone until its line"]
  style H1 fill:#1e7f3d,color:#fff
  style H2 fill:#b91c1c,color:#fff
Declarations are hoisted (callable early); const/let expressions sit in a dead zone until their line.
Function declarations are hoisted; const arrows are not.
console.log(typeof later);     // "function" — declaration hoisted to the top
console.log(later());          // "ok"
function later() { return "ok"; }

const now = () => "now";       // referenced only AFTER this line — safe
console.log(now());            // "now"
Exercise

What does this print? map squares each number into a new array.

const nums = [1, 2, 3];
console.log(nums.map((n) => n * n));
Exercise

Write longestWord(words) that returns the longest string in the array words. (Assume at least one word, and if two tie, return the first.)

function longestWord(words) {
  // return the longest string
}
Exercise

This builds three callbacks meant to each remember their own number — 0, 1, 2. But it uses var, so every callback closes over the same loop variable and they all return 3. Fix it by changing the one keyword that decides whether each iteration gets its own binding.

function makeHandlers() {
  const handlers = [];
  for (var i = 0; i < 3; i++) {
    handlers.push(() => i);
  }
  return handlers;
}
Exercise

What does this print? A function declaration is hoisted, so it is callable before the line that defines it. (The out array collects the two values.)

const out = [];
out.push(typeof greet);    // before the definition line
function greet() { return "hi"; }
out.push(greet());
console.log(out.join(","));
Exercise

Write makeCounter() that returns an object with two methods: next() adds 1 to a private count and returns it, and reset() sets the count back to 0. The count must be private — it must not appear as a property on the returned object. Use a closure to hide it, the way makeAdder hid base above.

function makeCounter() {
  // return { next, reset } closing over a private count
}

Recap

  • function name(n) {} and const name = (n) => {} both define functions; a single-expression arrow returns automatically.
  • Parameter is the placeholder in the definition; argument is the value at the call. A missing return gives back undefined.
  • let/const are block-scoped; var is function-scoped and shared across loop iterations — the root of the closure trap.
  • The scope chain lets an inner function read outward names; outer scopes cannot see in. A local name shadows an outer one.
  • A closure is a function that keeps an outer variable alive after that outer function has returned.
  • Declarations hoist (callable early); const/let arrows do not — they sit in a dead zone until their line.

Next you'll meet higher-order functions — functions that accept or return other functions — which is exactly what map and makeAdder were quietly doing all along.

Checkpoint quiz

What does the arrow function (n) => n * 3 return when called with 4?

A loop uses var i and pushes () => i into an array each iteration. After the loop, what does each callback return?

What does a function return if it runs to the end with no return?

Go deeper — technical resources