← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 17 — Functions and Scope

Section 1 — What a function is

A function is a named, reusable block of code that performs a job. You define it once, giving it a name and a body — the statements between its braces — and then you call it whenever that job needs doing, by writing its name followed by brackets. The power of that arrangement is hard to overstate, because it is what turns a program from a single long list of instructions into a collection of named jobs, each understandable on its own. A program that greets users, calculates totals, validates forms and displays results can be organised as four functions, each with a clear name and a single responsibility, rather than as one undifferentiated sequence where every concern is tangled with every other. When something goes wrong with greeting, you look at the greeting function. When the total is wrong, you look at the total function. That localisation of responsibility is the primary benefit of functions, and it is why every programming language has them in some form.

Functions also embody the principle of not repeating yourself. Without functions, a job that must be done in three places must be written three times, and each copy must be maintained separately: a fix applied to one copy but not the others produces inconsistent behaviour, and a reader must compare three copies to be sure they are the same. With a function, the job is written once, and each place that needs it calls the function, so a fix in one place fixes every use, and a reader who understands the function understands all of its uses. That economy is not just about saving keystrokes; it is about reducing the surface area where bugs can live. One copy of a job can contain a bug, but three copies can contain three different bugs, and the effort of keeping them consistent is effort spent on bookkeeping rather than on solving problems.

There is a third benefit that is easy to overlook: naming. A function with a good name — calculateTotal, greetUser, isValidEmail — gives a name to an idea, and code that calls it reads like a description of what the program does rather than a list of how it does it. A line that reads showWelcomeMessage(userName) is understandable at a glance, while the ten lines that implement the welcome message are understandable only on careful reading. Functions let a program be read at two levels: the high level, where function calls describe the flow, and the low level, where function bodies describe the details. That layered readability is what makes large programs comprehensible, and it starts with the humble decision to wrap a job in a function and give it a clear name.

Check: Why write a function instead of repeating the same code three times?

Section 2 — Defining a function

Defining a function means writing its recipe: its name, its inputs, and the statements that make up its job. The classic form, called a function declaration, starts with the keyword function, followed by the function's name, followed by brackets that will hold its parameters, followed by a block of statements in braces. The definition itself does nothing when it runs past it; it merely registers the function, making it available for later calls. That distinction — defining versus calling — is fundamental, and confusing the two is a common beginner error: a program that defines a function but never calls it will run without error and produce no result from that function, because the function's body never executed. Definition is writing the recipe; calling is cooking the dish.

function greet() {
  console.log("Hello!");
}

Choosing a function's name follows the same principles as naming a variable, with one addition: function names are usually verbs or verb phrases, because functions do things. CalculateTotal, greetUser, fetchData, validateForm — each name says what the function does, in a form that reads naturally when called: calculateTotal() reads as an instruction, "calculate the total", which is exactly what the call means. A noun name, like total or user, suggests a value rather than an action, and it misleads readers about what the name refers to. Verb names also help you notice when a function is doing too much: a name like doEverything or processStuff is vague because the function itself is vague, and the vagueness of the name is a signal that the function should be split into smaller, better-named pieces.

The body of a function is an ordinary block of code, and it can contain any statements: declarations, calculations, conditions, loops, and calls to other functions. That last point is important, because functions calling functions is how programs are layered: a high-level function orchestrates by calling lower-level functions, each of which does a smaller job, and those may call still smaller ones. A function that calculates an order total might call a function that calculates tax and a function that calculates shipping, each of which is simple on its own. That layering is what keeps each function short and understandable, and it is the practical form of the "one job per function" principle from Section 34.

A final detail about definitions: a function declaration is hoisted, meaning it is available throughout its scope even before the line where it is written, a behaviour Section 26 explores. That means you can call a function declaration before its definition in the file and it will work, which allows a style where the high-level flow appears first and the detailed functions follow below. Whether you use that style or define functions before their calls is a matter of taste and codebase convention, but the important thing is to be deliberate: organise functions so that a reader can find the flow first and the details second, and the program will be easier to follow for it.

Check: What is the difference between defining a function and calling it?

Section 3 — Calling a function

Calling a function means asking it to run. You write its name followed by brackets, and JavaScript executes the function's body, then returns to the line after the call and continues. The brackets are not decoration; they are the call operator, the signal that distinguishes using a function from merely mentioning it. Writing greet without brackets refers to the function itself, as a value, which is a capability Section 30 explores. Writing greet() runs the function. That distinction — name alone versus name with brackets — is small in appearance and large in consequence, and forgetting the brackets is one of the most common beginner errors, producing the symptom of a function that "does nothing" when in fact it was never called.

greet(); // runs the greet function

A call can appear anywhere an action is needed: at the top level of a script, inside another function, inside a condition, inside a loop. Each call runs the body afresh, with its own execution, and the function does not remember anything from previous calls unless it deliberately stores state outside itself, which is a design choice with consequences that Section 15 and the closure sections discuss. That freshness is what makes functions reliable: calling calculateTotal with the same inputs gives the same result every time, because each call starts from the same beginning and follows the same steps. A function that behaved differently on its second call without any change in inputs would be surprising and hard to debug, and the default freshness of function calls is a guard against that surprise.

Calls can also be nested and chained in ways that reward careful reading. A call inside another call's argument, like console.log(calculateTotal(price, tax)), first runs the inner function, then passes its result to the outer one. Reading such a line inside-out — inner result first, outer action second — is the same skill as reading nested expressions from the previous chapter. When nesting becomes deep enough to be hard to read, the remedy is to break it into steps with intermediate variables, naming each result, which is clearer and easier to debug, because each intermediate value can be logged or inspected. Clarity beats compactness, especially in calls, where the flow of values between functions is the program's logic made visible.

Finally, a call's timing matters. A function called at load time runs when the page loads. A function called in response to an event runs when the event happens. A function called inside a loop runs once per iteration. Bugs often involve timing: a function that works when called manually from the Console but not when the page runs may be called too early, before the data or elements it needs exist, or may not be called at all, because the event that should trigger it is not wired up. When a function seems not to work, the first question is always whether it ran, and the debugging chapter's tools — a log as the function's first line, a breakpoint on its first line — answer that question directly. A function that never runs cannot produce a result, no matter how correct its body is.

Check: What is the difference between writing greet and writing greet()?

Section 4 — Parameters

Parameters are the inputs a function declares it needs, written inside the brackets of its definition. They are placeholders: names that will hold the actual values when the function is called. A function defined as function greet(name) declares one parameter, name, and inside the function's body, name refers to whatever value the caller supplied. Parameters are what make functions general rather than specific: instead of a function that greets only Ada, you write a function that greets whatever name it is given, and the parameter is the mechanism by which the specific value arrives. Without parameters, functions would be limited to fixed jobs with fixed data, and much of their power would vanish.

Parameters behave like variables declared inside the function: they are born when the function is called, they hold the values supplied by the caller, and they disappear when the function returns. They are also local to the function, which is a scope concept Section 17 explores in full: code outside the function cannot see or use its parameters, and code inside the function uses them freely. That locality is a feature, because it means a parameter name cannot clash with names elsewhere in the program. A function can have a parameter called total without affecting a variable called total elsewhere, because the parameter lives only inside its function. That isolation is part of what makes functions safe to reuse: their inputs arrive through a defined channel, and they do not interfere with the rest of the program's names.

Choosing parameters is a design decision. A function should take as parameters exactly the values it needs to do its job, and no more. A greeting function needs a name. A total function needs prices and quantities. A validation function needs the value to validate and the rule to apply. Parameters that are not needed clutter the call and confuse readers about what the function actually uses. Parameters that are missing force the function to reach outside itself for data, which is the global-reliance problem Section 35 warns against: a function that reads a global variable instead of taking a parameter is harder to test, harder to reuse, and harder to understand, because its true inputs are hidden. The parameter list is the function's honest declaration of what it needs, and honesty there makes everything else easier.

There is a practical point about the number of parameters. A function with one or two parameters is easy to call correctly. A function with five or six parameters is easy to call incorrectly, because the caller must remember the order and the meaning of each one, and a swapped pair produces a bug that may not be obvious. When a function seems to need many parameters, that is often a sign that it is doing too much and should be split, or that several parameters belong together as a single grouped value, an object, which a later chapter covers. For now, the guideline is to keep parameter lists short, to name parameters clearly, and to let the list itself document what the function needs. A short, clear parameter list is a sign of a well-designed function.

Check: What is a parameter, and how is it different from a value?

Section 5 — Arguments

Arguments are the actual values supplied to a function when it is called, filling its parameters. If a function is defined as function greet(name), then the call greet("Ada") supplies the argument "Ada", which becomes the value of the parameter name inside that call. The terminology is precise and worth getting right, because confusing the two terms makes discussions about functions confusing: parameters are the placeholders in the definition, arguments are the values in the call. A helpful mnemonic is that parameters are part of the function's recipe, while arguments are the ingredients supplied on a particular occasion. The same recipe, greet, can be cooked with different ingredients — "Ada", "Ben", "Chidi" — and each call produces a different result from the same definition.

function greet(name) {
  return `Hello, ${name}!`;
}
greet("Ada"); // argument "Ada" fills parameter name

Arguments are evaluated before the function runs, in the caller's context, which means an argument can be any expression: a literal, a variable, a calculation, or another function call. The call greet("Ada" + " Okafor") evaluates the concatenation first, then passes the result. The call calculateTotal(price * quantity, taxRate) evaluates the multiplication first, then passes the product. That evaluation order matters when an argument expression has side effects or depends on values that change, but in ordinary code it simply means you can pass computed values as easily as literal ones, which keeps calls flexible. A function that takes a total does not care whether the total was typed literally, read from a variable, or calculated on the spot; it receives the value, and that indifference is part of what makes functions reusable.

The number of arguments should match the number of parameters, and JavaScript is forgiving when it does not, in ways that can hide mistakes. Extra arguments are ignored, unless the function deliberately inspects them, which a later chapter's rest parameters formalise. Missing arguments leave their parameters holding undefined, which usually produces a confusing result downstream rather than an immediate error: a greeting function called without a name produces "Hello, undefined!", which is technically a result but not a useful one. That forgiveness is why default parameters, covered in Section 11, are valuable: they give missing arguments a sensible value instead of undefined, turning a silent oddity into a defined behaviour. Until then, the discipline is to check calls against definitions: count the arguments, compare them with the parameters, and make sure each argument is the kind of value its parameter expects.

Order matters as much as count. Arguments fill parameters in order: the first argument fills the first parameter, the second fills the second, and so on. Swapping two arguments of the same type — passing (tax, price) where (price, tax) was expected — produces a result that is wrong but not obviously so, because both values are numbers and no error occurs. That class of bug is prevented by clear parameter names, by keeping parameter lists short, and by testing functions with values whose results you can verify independently. When a function's result is wrong but its logic looks right, checking the argument order at the call site is a worthwhile early step, because the function may be correct and the call may be the problem.

Check: What is the difference between a parameter and an argument?

Section 6 — Return values

A return value is the result a function hands back to its caller. Inside a function, the return statement specifies that value and ends the function's execution immediately: any statements after a return in the same block do not run, because the function has already finished. The caller receives the returned value as the result of the call expression, and can store it, display it, calculate with it, or pass it to another function. That handoff — a function computes something, returns it, and the caller uses it — is the primary way functions communicate results, and it is what makes functions composable: the output of one function can become the input of another, chaining small jobs into larger ones.

function double(number) {
  return number * 2;
}
let result = double(5); // result is 10

Return values are what distinguish a function that answers a question from a function that performs an action. A function that calculates a total should return the total, so the caller can decide what to do with it: display it, store it, compare it, or use it in a further calculation. A function that only prints the total, without returning it, traps the result inside itself: the caller can see the printed value but cannot use it for anything else, and testing the function requires capturing its printed output rather than checking a returned value. That distinction is a design principle worth internalising early: prefer functions that return values over functions that only produce side effects, because returned values are reusable and testable in ways that printed output is not. A function can both return a value and perform an action, but when it must choose, returning is usually the more useful choice.

There are several details about return that are worth knowing precisely. A function without a return statement, or with a return statement that specifies no value, returns undefined, which is the language's way of saying "this function produced no result". That is correct for functions whose job is purely an action, like displaying a message, but it is a bug when the function was supposed to produce a result and a code path forgot to return it — a common cause of "undefined" appearing where a value was expected, which the debugging chapter's techniques can trace. A return statement ends the function at once, which makes it useful for early exits, as Section 13 explains: when a function encounters a case it cannot or need not handle further, it can return immediately rather than nesting the rest of its logic inside a condition. And a function can have several return statements, one per case, as long as each case's logic is clear, though a proliferation of returns can make a function's flow harder to follow, so they are best used deliberately rather than scattered.

Check: What does a return statement do, besides specifying a value?

Section 7 — Functions without a return value

Not every function returns a meaningful value. Some functions exist purely to perform an action: display a message, update the page, save a value, trigger an animation. Such a function may have no return statement at all, or a bare return used only to exit early, and calling it gives the value undefined. That is not an error and not a flaw; it is the language's honest answer to "what did this function produce?" when the function produced an action rather than a value. The distinction between action functions and value functions is a useful way to think about a program's design: value functions answer questions and are reusable in calculations, while action functions change the world and are called for their effects.

The key discipline for action functions is to be clear, in the name and in the documentation, that the function performs an action rather than returning a value. A name like showMessage or saveData signals an action, while a name like calculateTotal or getUserName signals a value, and a reader who sees a call to showMessage does not expect to store its result, while a reader who sees calculateTotal does. Confusion arises when the two kinds are mixed: a function named getTotal that also displays the total, or a function named displayTotal that also returns it, forces readers to remember both behaviours, and a caller that stores the result of an action function will hold undefined and wonder why. Naming a function for its primary job, and letting that job be either a value or an action, keeps the contract clear.

There is a subtle bug pattern associated with missing returns that deserves explicit attention. A function intended to return a value will return undefined if any code path through it fails to reach a return statement. Consider a function with a condition: if the condition is true it returns a value, but if it is false it falls through to the end without returning, giving undefined. The caller, expecting a value in all cases, receives undefined in the false case and fails downstream, far from the function that caused the problem. The remedy is to ensure every path returns: either a value for each case, or a sensible default at the end of the function that covers any path not handled earlier. Reviewing a function by tracing each path to its end — does this path return, does that path return — is a quick check that catches this class of bug before it ships. That distinction also helps when reading other people's code: a function that returns a value can be used in an expression, while an action function cannot.

A function that does both should say so in its name, or it will surprise its callers.

Check: What value does a function return if it has no return statement?

Section 8 — Function expressions

A function declaration, covered in Section 2, defines a function as a statement, giving it a name that can be called elsewhere. A function expression defines a function as a value, usually by assigning it to a variable, and the function may or may not have its own name. The form is familiar: const greet = function(name) { return Hello, ${name}!; }; creates a function and stores it in the variable greet, and greet("Ada") calls it exactly as if it had been declared. The difference is not in how the function behaves when called, but in how it comes into existence: a declaration creates a named function in its scope, while an expression creates a function value that can be stored, passed, and used like any other value, because in JavaScript functions are values.

const greet = function (name) {
  return `Hello, ${name}!`;
};

That "functions are values" idea is one of the most powerful in the language, and function expressions are the first place it becomes visible. Because a function can be stored in a variable, it can also be stored in a list, passed as an argument to another function, and returned from a function, all of which later sections and later chapters explore. A function stored in a variable is also subject to the variable's declaration rules: a const function expression cannot be reassigned, a let function expression can, and a function expression is not available before its declaration line, unlike a function declaration, which is hoisted. That last difference matters in practice: code that calls a function expression before its line will throw an error about accessing the variable before initialisation, while the same code with a declaration would work. The choice between declaration and expression is therefore partly about timing and partly about style.

Anonymous function expressions — those without a name of their own — are common as arguments to other functions, where the function is used once and a name would add little. An event handler written inline, or a short function passed to a method that processes a list, is often anonymous, because its context explains its purpose. Named function expressions exist too, and they have an advantage in debugging: when an error's stack trace shows a function's name, a named function is identifiable, while an anonymous function appears as anonymous, which is less helpful. Modern tools infer names for many anonymous functions from the variable they are assigned to, which reduces the problem, but giving a function a name when it will be reused or when its stack traces matter is still good practice.

The practical guidance is to use declarations for the main, named functions of a program — the jobs that deserve a name and a stable place in the code — and to use expressions when a function is being stored, passed, or created conditionally, or when the function expression style fits the surrounding code's conventions. Both forms define the same kind of thing, and a program will typically contain both. Understanding that they are two syntaxes for the same concept, differing in hoisting and in how the function is named and stored, removes a common source of confusion when reading code that mixes them, which is most real code.

Check: What is the difference between a function declaration and a function expression?

Section 9 — Arrow functions

Arrow functions are a shorter syntax for writing function expressions, introduced to make small functions concise and readable. Instead of the function keyword, an arrow function uses its parameters, followed by =>, followed by its body. The form const double = (number) => number * 2; defines a function that takes a number and returns twice it, in a single line, without the words function or return. When the body is a single expression, that expression's value is returned automatically, which is called an implicit return. When the body needs several statements, it is written in braces like an ordinary function, and return must be written explicitly. Arrow functions are not a different kind of function in most respects; they are a compact notation for the same idea, and they are now the dominant style for short functions in modern JavaScript.

const double = (number) => number * 2;

The conciseness rules have a few details worth learning precisely. A single parameter can omit its brackets: number => number * 2 is valid, though many codebases keep the brackets for consistency, and consistency within a codebase matters more than the saving of two characters. No parameters require empty brackets: () => "hello" is a function that takes nothing and returns "hello". A single-expression body returns implicitly, but a body in braces does not: (number) => { number * 2; } returns undefined, because the braces signal a block of statements, and a block needs an explicit return to produce a value. That trap — braces without return giving undefined — is the arrow function equivalent of the missing-return bug from Section 7, and it is worth checking whenever an arrow function produces undefined unexpectedly. Returning an object literal from a single-expression body requires wrapping the object in brackets, as in () => ({ name: "Ada" }), because otherwise the braces are read as a block rather than as an object, another small syntax detail that confuses beginners once and is then remembered.

Arrow functions differ from ordinary functions in one deeper respect, which Section 10 covers: their handling of this, the special value that refers to the current context. For the functions in this chapter, which do not use this, the difference does not arise, and arrow functions can be treated as a shorter way of writing the same thing. That is the right mental model for now: arrows for brevity, ordinary functions for the general case, with the this difference noted for later. As you read modern code, you will see arrow functions everywhere, particularly as short callbacks and as small utility functions, and fluency with their syntax — implicit return, single-parameter brackets, braces versus expression bodies — is essential to reading that code comfortably. Writing them well means using them where their brevity clarifies, and using an ordinary function with a name and braces where a longer body deserves the fuller form.

Check: What does const double = (number) => number * 2 return when called with 5?

Section 10 — Arrow functions and this

Arrow functions differ from ordinary functions in how they handle this, a special keyword that refers to the current execution context. In an ordinary function, this is determined by how the function is called: when a function is called as a method of an object, this refers to that object, and when it is called plainly, this refers to a default context that depends on the mode the code is running in. In an arrow function, this is not set by the call at all; instead, the arrow function inherits this from the surrounding code where it was defined. That inheritance is the reason arrow functions were introduced, because it solves a long-standing frustration with callbacks: an ordinary function used as a callback inside a method would lose the method's this, while an arrow function keeps it, because it never had its own this to lose.

The practical consequences for now are twofold. First, when you write a short callback inside a function or method and you need to refer to the surrounding context, an arrow function is the natural choice, because it preserves that context. That is why arrow functions dominate as callbacks in modern code: they behave intuitively inside other code, without the surprises that ordinary function callbacks historically produced. Second, when you write a method — a function that belongs to an object and is meant to use that object via this — an ordinary function is the right choice, because an arrow function used as a method would inherit this from outside the object rather than referring to the object itself, which is almost never what is intended.

A common beginner error is to use arrow functions for everything, including methods, and then to be confused when this does not refer to the expected object. Another is to use ordinary functions for callbacks and then to be confused when this changes inside the callback. Both errors stem from treating the two forms as interchangeable, when they differ precisely on this point. The rule of thumb that prevents both errors is to ask, for each function, whether it needs its own this. If the function is a method that should refer to its object, use an ordinary function. If the function is a callback or a small utility that should see the surrounding context, use an arrow function. If the function does not use this at all — which is true of most functions in this chapter — either form works, and the choice is stylistic.

You do not need to master this today. You need to know that the difference exists, that it is the one significant difference between arrow and ordinary functions, and that it explains why both forms coexist in modern code rather than one replacing the other. When you reach the objects chapter and this becomes central, this section will be the foundation you build on. Until then, write arrow functions for short callbacks and utilities, write ordinary functions for named jobs and methods, and let the distinction grow on you through practice rather than through memorisation.

Check: How does an arrow function's this differ from an ordinary function's this?

Section 11 — Default parameters

A default parameter gives a parameter a value to use when the caller does not supply an argument for it. The syntax places an equals sign and a default value after the parameter name in the definition: function greet(name = "Guest") declares that name defaults to "Guest". Calling greet("Ada") gives name the value "Ada", as usual. Calling greet() with no argument gives name the value "Guest", rather than undefined. That small mechanism removes a whole class of "undefined" results and the defensive code that used to guard against them, because the function now defines its own fallback behaviour instead of relying on the caller to always supply every argument.

function greet(name = "Guest") {
  return `Hello, ${name}!`;
}
greet(); // "Hello, Guest!"

Defaults apply only when the argument is missing or explicitly undefined. Calling greet(undefined) triggers the default, because undefined means "no value supplied", which is consistent with the meaning of undefined from the values chapter. Calling greet(null), however, does not trigger the default, because null is a deliberate value, not an absence, and the function receives null and must handle it as it would any other value. That distinction — undefined triggers a default, null does not — mirrors the undefined-versus-null distinction generally, and it is worth knowing because it determines whether a default protects you in a given case. When a value might be null, a default parameter alone is not enough, and the function needs an explicit check for null as well.

Default values can be any expression, not just literals, and they are evaluated at call time, which means they can refer to earlier parameters and to values available when the function runs. A function defined as function createUser(name, role = "member") gives every user the member role unless another is specified, which is a natural way to express a common case with exceptions. A function defined as function greet(name, greeting = Hello, ${name}) builds its default greeting from the name parameter, showing that defaults can depend on other parameters, provided those parameters come earlier in the list. That flexibility makes defaults a design tool, not just a safety net: they express the common case directly in the function's signature, where a reader can see it without reading the body.

There are limits to keep in mind. A default does not validate its input: a caller who passes a wrong type still passes that wrong type, and the default does not intervene, because the argument was supplied. Defaults also do not help when too many arguments are supplied, or when arguments are supplied in the wrong order, which are caller errors that defaults cannot detect. And a default that is a mutable value, like an object or an array, is freshly created on each call in JavaScript, unlike in some other languages where a default object would be shared between calls, so that particular trap does not apply here. Used within those limits, default parameters make functions more forgiving to call, more self-documenting in their signatures, and less cluttered in their bodies, because the fallback logic lives in one place — the parameter list — rather than scattered through the function as checks for undefined.

Check: What happens when you call greet() if greet is defined with name = "Guest"?

Section 12 — Rest parameters

Rest parameters collect any number of extra arguments into a single array, using three dots before the parameter name in the definition. A function defined as function sum(...numbers) can be called with any number of arguments — sum(1, 2), sum(1, 2, 3, 4, 5), sum() with none — and inside the function, numbers is an array holding all the arguments supplied. Without rest parameters, such functions would need to accept a single array argument, which is workable but less convenient to call, because the caller must wrap its values in brackets.

function sum(...numbers) {
  let total = 0;
  for (let n of numbers) {
    total += n;
  }
  return total;
}
sum(1, 2, 3); // 6

Rest parameters have a few rules that define their shape. A function can have only one rest parameter, and it must be the last parameter in the list, because it collects everything that remains after the earlier parameters have been filled. A function defined as function describe(title, ...tags) takes a title as its first argument and collects all further arguments into tags, which is a natural pattern: a fixed first input plus a variable tail. Calling such a function with only a title gives an empty array for tags, not undefined, which is convenient, because code that processes tags can iterate over an empty array without a special case. The rest array is a genuine array, with all array methods available, which the arrays chapter will explore, and for now it is enough to know that it can be looped over, measured with length, and indexed like any array.

Rest parameters replaced an older mechanism, the arguments object, which was available inside every ordinary function and held all arguments passed, but which was not a real array and had confusing behaviour. Rest parameters are clearer because they are declared in the signature, where a reader can see that the function takes variable arguments, and because they produce a real array. Arrow functions do not have an arguments object at all, which is another reason rest parameters are the modern approach: they work identically in ordinary and arrow functions. When you meet arguments in old code, read it as the predecessor of rest parameters, and when you write new code that takes variable arguments, use rest parameters.

The design judgement for rest parameters is about when variable arguments are genuinely appropriate. A function that totals prices is a natural fit, because the number of prices is inherently variable. A function that takes a fixed set of distinct inputs — a name, an age, and an address, for example — is not a fit, because those inputs have distinct meanings and should be distinct parameters, so that a caller cannot accidentally swap them or omit one silently. Rest parameters are for homogeneous lists, where each argument is another of the same kind of thing. Used for homogeneous lists, they make calls clean and functions flexible. Used for heterogeneous inputs, they hide the function's true requirements behind a single array, and a caller cannot tell from the signature what the function needs. That distinction — homogeneous list versus distinct inputs — is the guide to using rest parameters well.

Check: What does ...numbers collect inside a function?

Section 13 — Returning early

Returning early means placing a return statement before the end of a function, so that the function exits as soon as a particular case is handled, rather than nesting the rest of its logic inside a condition. The classic pattern is a guard clause: at the top of a function, check for a case that needs no further work — invalid input, a missing value, an already-finished state — and return immediately, often with a sensible value or with nothing, so that the rest of the function can assume the case does not apply. That structure flattens code: instead of a function whose body is one large conditional with the real logic indented inside it, you get a function whose early exits clear away the edge cases, leaving the main logic at the top level, unindented and easy to follow.

function divide(a, b) {
  if (b === 0) {
    return null;
  }
  return a / b;
}

Early returns are sometimes criticised on the grounds that a function should have a single exit point, a rule from older programming practice when functions were longer and control flow was harder to follow. In modern JavaScript, with short functions and clear structure, the consensus has shifted: several well-placed early returns, each handling a distinct case, are clearer than a single return at the end of a deeply nested structure, because each early return removes a case from consideration and lets the reader stop thinking about it. The reader's mental load decreases with each guard clause, because the remaining code operates under fewer assumptions. A function with three guard clauses and a short main body is easier to understand than a function with one return and three levels of nesting, even though the latter has fewer exits.

There are disciplines that keep early returns clean. Each early return should handle a complete case, with a value appropriate to that case, so that the function's contract — what it returns in each situation — is clear. Early returns should appear near the top of the function for input validation and edge cases, rather than scattered through the middle, so that a reader can see the cases that are excluded before reading the main logic. And early returns should not be used to hide complexity: if a function has so many early returns that its flow is hard to follow, the function is probably doing too much and should be split, with each piece handling its own cases. Early return is a structuring tool, not a substitute for decomposition.

A final benefit of early returns is in debugging. When a function returns unexpectedly, the early return that fired is identifiable by tracing which guard clause's condition was true, and each guard clause can be checked independently: was the input really invalid, was the state really finished, was the value really missing? That traceability makes early-return functions easier to debug than nested functions, where an unexpected result might come from any level of nesting. Returning early is, in short, a way of saying "this case is done" as soon as it is done, rather than carrying it through the rest of the function as a condition to be remembered. That promptness, applied consistently, makes functions shorter, flatter, and easier to reason about.

Check: What is a guard clause, and why is it useful?

Section 14 — Pure functions and side effects

A pure function is one whose result depends only on its inputs, and which changes nothing outside itself. Given the same arguments, it always returns the same result, and it does not modify any variable outside itself, write to the page, log to the Console, or affect any other part of the program. A function that doubles a number is pure: double(5) is always 10, and calling it changes nothing else. A function that reads the current time is not pure, because its result depends on something outside its arguments. A function that updates the page is not pure, because it changes the world. Purity is not a moral judgement; it is a description of a function's relationship with the rest of the program, and pure functions are prized because that relationship is the simplest possible one.

A side effect is anything a function does besides returning a value: changing a variable outside itself, modifying the page, writing to storage, printing to the Console, making a network request. Side effects are necessary — a program that never changed anything would be useless — but they are also the source of much complexity, because a function with side effects cannot be understood from its inputs and outputs alone. Its behaviour depends on the state of the world when it runs, and its running changes that state for everything else. A function that increments a global counter, for example, returns a different result on each call, not because its arguments changed, but because the counter changed, and a reader who does not know about the counter cannot predict the function's behaviour from its signature. That hidden dependence is what makes side effects costly to reason about.

The practical approach is not to eliminate side effects, which is impossible, but to separate them from pure logic as much as possible. Put calculations in pure functions, which can be tested easily, reused safely, and reasoned about from their signatures alone. Put side effects in dedicated functions whose job is explicitly to change the world — display a result, save a value, update the page — and let those functions call the pure functions for their calculations. That separation, sometimes called pushing side effects to the edges, means most of a program's logic lives in pure, predictable functions, while the impure parts are few, visible, and named for their effects. A program organised that way is easier to test, because its pure functions can be tested without setting up a page or a network, and easier to debug, because a wrong result can be traced to either a pure calculation, which is deterministic, or a side effect, which is visible in its dedicated function. Purity is a spectrum in real code, and most functions fall somewhere between perfectly pure and heavily effectful.

Check: What makes a function pure?

Section 15 — Side effects in practice

The previous section defined side effects and argued for separating them from pure logic. This section looks at side effects as they actually appear in front-end code, because recognising them in the wild is a different skill from defining them. The most common front-end side effect is changing the page: setting an element's text, adding or removing a class, showing or hiding a section. Those changes are the point of an interactive page, and they are side effects in the precise sense that they change something outside the function — the document — rather than returning a value. A function called showMessage that sets a paragraph's text is an action function, and its effect is visible on the page, which is exactly what the user wants, even though it makes the function impure by the definition.

Other common side effects include logging to the Console, which changes the Console's contents; storing a value, which changes storage; making a network request, which changes a server's state or at least consumes network resources; and modifying a variable outside the function, which changes the program's state for every other function that reads that variable. That is why the previous chapter's advice to prefer parameters over globals matters: parameters make a function's inputs visible, while global modification hides them.

Managing side effects in practice means making them explicit and contained. Name functions that perform side effects for their effects: displayTotal, saveUser, fetchData, updateCart. Keep each side-effecting function focused on one effect, so that its behaviour is predictable from its name. Avoid side effects in functions that are also expected to return a value, unless the effect is the function's documented secondary purpose, because a caller that stores the return value may not expect the page to have changed as well, and a caller that calls the function for its effect may not expect to handle a return value. And when a side effect is unavoidable inside a calculation — for example, a function that must log a warning when it encounters bad data — make the effect visible, either in the name or in a comment, so that a reader is not surprised by it.

There is also a testing consequence that makes side-effect management practical rather than theoretical. A pure function can be tested by calling it with inputs and checking its output, with no setup. A side-effecting function requires setup — a page to modify, storage to inspect, a network to mock — and its tests are slower and more fragile. A codebase with mostly pure functions and a few well-contained side effects is therefore a codebase that is cheap to test, which means it is actually tested, which means its bugs are caught earlier. That chain, from purity to testability to quality, is the real reason professional developers care about side effects, and it is a reason that becomes more compelling with every program you maintain. Side effects are necessary; uncontrolled side effects are expensive.

Check: Name three common side effects in front-end code.

Section 16 — What scope means

Scope is the region of a program where a name is visible and usable. A variable declared in one place can be used in some parts of the program and not in others, and scope is the rule that decides which parts those are. The simplest way to think about it is as a set of nested boxes: a name declared in a box is visible inside that box and inside any smaller boxes nested within it, but not outside it. A variable declared at the top level of a script is in the outermost box and is visible everywhere. A variable declared inside a function is in that function's box and is visible only inside that function. A variable declared inside a block is in that block's box and is visible only inside that block. Those three boxes — global, function, and block — are the three scopes you will work with, and the rest of this chapter's scope sections explore each one and their interactions.

Scope exists for the same reason that functions exist: to limit what each part of a program can see and change, so that parts can be understood and modified independently. Without scope, every variable would be visible everywhere, and every part of a program could change every variable, which would make reasoning about any part require understanding the whole program. With scope, a function can use a variable name without worrying that another function uses the same name differently, because each function's names live in its own box. That isolation is what makes it safe to reuse names — a loop counter called i in one function does not interfere with a loop counter called i in another — and what makes it possible for different developers to work on different functions without coordinating every variable name.

Scope is determined by where a variable is declared in the code, not by where it is used at runtime, which is called lexical scope or static scope. That means you can determine a variable's visibility by reading the code, without running it: find the declaration, find the enclosing box, and everything inside that box can see the name. That readability is a feature: scope is a property of the text, not of the execution, so a reader can answer "can this code see that variable?" by looking at the structure of the code. Languages with dynamic scope, where visibility depends on who called whom at runtime, are harder to read for exactly that reason, and JavaScript's choice of lexical scope is one of the decisions that makes it tractable to understand. That nesting rule is the whole of scope in one sentence, and every later complication in this chapter is a consequence of it.

Check: What is scope, in one sentence?

Section 17 — Global scope

Global scope is the outermost box: a variable declared at the top level of a script, outside any function or block, is global, and it is visible everywhere in that script, inside every function and every block. Global variables are the program's shared state, accessible to all parts, and that accessibility is both their use and their danger. They are useful for values that genuinely belong to the whole program: configuration that every part needs, a reference to a central element, a constant that never changes. A global constant like const TAX_RATE = 0.15, declared at the top level, can be read by any function that calculates tax, without being passed as a parameter, and that convenience is legitimate when the value is truly program-wide and truly constant.

The danger of globals is that any part of the program can change a global variable, and every other part will see the change, which creates hidden connections between parts that appear independent. A function that modifies a global counter affects every other function that reads that counter, and a reader of any one function cannot tell, from that function alone, what the counter's value will be when it runs, because it depends on which other functions ran before it and in what order. That order-dependence is what makes programs with many mutable globals hard to debug: the bug may be in the function that changed the global, not in the function that read it, and the distance between cause and symptom can be large. Mutable globals are therefore a liability that grows with program size, and professional codebases minimise them, preferring parameters and return values to carry data between functions explicitly.

There is a distinction within globals that matters: global constants are far safer than global variables that change. A global const cannot be reassigned, so it cannot be changed by any part of the program, and its value is the same everywhere, which removes the order-dependence problem. A global let that changes is the dangerous kind, because its value at any moment depends on history. That distinction is another reason for the const-by-default rule from the previous chapter: it makes even global declarations safer, by ensuring that the globals which do exist are mostly constants. When a global must change — a genuinely program-wide state, like whether a user is logged in — it should be changed through dedicated functions whose names make the change visible, rather than by direct assignment scattered through the code, so that the change points are few and findable. A global constant is a promise to every reader that the value will not change, which is why constants are the safest globals.

Treat a changing global as a warning sign, and look for a way to pass the value instead.

Check: Why are mutable global variables risky?

Section 18 — Function scope

Function scope is the box created by a function: variables declared inside a function are visible throughout that function, including inside any blocks nested within it, but not outside it. When a function is called, its local variables are created fresh; when it returns, they are discarded, unless a closure, covered in Sections 23 and 24, keeps them alive. That lifecycle — born on call, dead on return — is what makes function scope safe: a function can use any names it likes internally without affecting the rest of the program, and the rest of the program can use the same names without affecting the function. Two functions can each have a variable called total, and those totals are entirely separate, because each lives in its own function's box.

Function scope is also where parameters live. A function's parameters are, in effect, local variables that are initialised with the arguments supplied by the caller, and they follow the same visibility rules as other locals: visible inside the function, invisible outside it. That is why a parameter name cannot clash with a variable elsewhere: the parameter's box is the function, and names outside that box are in different boxes. It is also why a function cannot be understood from outside by inspecting its parameters: parameters are part of the function's interior, and the only way to know what a function does with them is to read its body or to test it with arguments and observe its return value.

Variables declared with var are function-scoped, which is the historical behaviour that Section 9 of the previous chapter mentioned: a var declared inside a block within a function is visible throughout the function, not just within the block. Variables declared with let and const are block-scoped, which is stricter, and Section 19 covers that distinction. For most modern code, written with let and const, function scope is the outer box within which block scopes nest: a let declared at the top of a function body is visible throughout the function, because its block is the function's body block, while a let declared inside an if block within the function is visible only inside that if block. Understanding function scope as the container for block scopes, rather than as a competing rule, unifies the two: blocks nest inside functions, and visibility follows nesting.

The practical benefit of function scope is encapsulation: a function is a self-contained unit whose internal names do not leak. When you write a function, you can choose variable names for clarity within that function, without checking every other function for collisions. When you read a function, you can assume its local names are its own, and you need only check its parameters and its return value to understand its interface with the rest of the program. That encapsulation is what makes functions reusable: a function that depends only on its parameters and its locals can be moved to another program and work identically, because it carries no hidden dependencies on the surrounding code's names. Function scope is the mechanism that makes that portability possible.

Check: What happens to a function's local variables when it returns?

Section 19 — Block scope with let and const

Block scope is the box created by a pair of braces that is not a function's body: the braces of an if statement, a loop, or a bare block. Variables declared with let or const inside such a block are visible only inside that block, and not outside it. That rule is stricter than var's function scope, and it is the reason let and const are safer: a variable declared for use inside a single if branch cannot leak out and be used, or misused, after the branch. The variable's life is confined to the region where it is needed, which matches the intuition that a temporary value should not outlive its purpose.

if (true) {
  let message = "inside";
  console.log(message); // works
}
console.log(message); // Error: message is not defined

Block scope is most visible in loops, where a variable declared in the loop's header or body is fresh for each iteration when declared with let, a behaviour Section 44 explores in the context of closures. It is also visible in conditions, where a variable computed for one branch should not be available in another branch, because the branches represent different situations. Confining a variable to its branch prevents a common error: using a value computed for one case in another case where it does not apply, because the variable is simply not visible there, and the error that results from trying is immediate and clear, rather than a silent wrong value.

The interaction between block scope and function scope follows the nesting rule from Section 16: a block inside a function can see the function's variables, because the function's box encloses the block's box, but the function cannot see the block's variables, because the block's box is smaller and does not enclose the function. That asymmetry — inner can see outer, outer cannot see inner — is the fundamental direction of scope visibility, and it applies at every level of nesting. A variable declared in an outer box is visible in all inner boxes; a variable declared in an inner box is visible only there. Reading code with that rule in mind turns scope questions into nesting questions: find the declaration, find its box, and ask whether the use is inside that box.

A practical consequence is that declaring variables in the smallest box that needs them is good practice. A counter needed only inside a loop should be declared inside the loop, not at the top of the function, because its confinement signals its purpose and prevents its accidental use elsewhere. A temporary value needed only inside a condition should be declared inside that condition's block. That discipline — smallest sufficient scope — reduces the number of names a reader must track at any point, because names that are not relevant to the current code are simply not visible there. It also reduces bugs, because a name cannot be misused in a place where it does not exist. Block scope is the language's tool for that discipline, and let and const are the declarations that use it.

Check: Why is a let variable declared inside an if block not visible outside it?

Section 20 — var and function scope

Var's scoping rule is the historical exception to the block-scope story: a variable declared with var is scoped to its enclosing function, not to its enclosing block. A var declared inside an if block, a loop, or any other block within a function is visible throughout that function, including before the block in execution terms, where it holds undefined due to hoisting, and after the block, where it holds whatever value the block assigned. That behaviour surprises developers who expect block scoping, because it means a variable that looks confined to a small region is actually available everywhere in the function, and a variable that looks like it belongs to one iteration of a loop is actually shared across all iterations, which is the closure-in-a-loop problem Section 45 examines.

The reason for var's behaviour is historical: when JavaScript was created, it had functions and a global scope, but block scope as a concept was not part of the design, and var was defined in terms of the scopes that existed. When let and const were introduced years later, they were given block scope deliberately, to fix the problems var's function scope caused, and var was left unchanged for backwards compatibility. That history explains why two scoping rules coexist in the language, and why modern advice is uniformly to use let and const: they implement the scoping that developers expect and that produces fewer surprises. Var remains in the language because removing it would break existing programs, not because it is recommended.

When reading old code that uses var, translate its scoping mentally: a var inside a block should be read as if it were declared at the top of its function, because that is where it is visible from, even though its assignment happens where it is written. That mental translation resolves many confusions in old code, particularly the loop-variable problem, where a var loop counter used inside a callback refers to the shared counter rather than to the iteration's value. When writing new code, the rule is simply not to use var, so that the translation is never needed and the scoping in your code matches the scoping a reader expects from the braces they see. Braces that create a visible box should create a scope box, and with let and const they do.

There is one more var behaviour worth knowing for reading old code: a var declared at the top level becomes a property of the global window object, as Section 17 mentioned, which means it is visible to all scripts on the page and can be overwritten by another script that chooses the same name. Let and const globals do not become window properties, which is another small safety advantage. None of these details needs to change how you write new code — use let and const, declare in the smallest sufficient scope — but all of them help you read old code accurately, and reading old code accurately is a large part of maintaining real programs, which are full of history.

Check: How does var's scope differ from let's scope inside a block?

Section 21 — Shadowing

Shadowing happens when a variable declared in an inner scope has the same name as a variable in an outer scope. Inside the inner scope, the inner variable hides — shadows — the outer one, so that any use of the name refers to the inner variable, not the outer. Outside the inner scope, the outer variable is visible again, unchanged by anything the inner code did to its own variable. Shadowing is not an error; the language permits it, and it happens naturally when a function uses a parameter or a local with the same name as a global, or when a block declares a variable with the same name as a function-level variable. But it is a frequent source of confusion, because a reader who does not notice the inner declaration will assume a use of the name refers to the outer variable, and will misunderstand the code.

let name = "Outer";
function show() {
  let name = "Inner";
  console.log(name); // "Inner"
}
show();
console.log(name); // "Outer"

A function that takes a parameter called data, while the program also has a global called data, is shadowing, but the function's parameter is the data that function should use, and the shadowing is a natural consequence of the function being self-contained. A loop counter called i inside a function shadows nothing important, because i is a conventional name for a counter and no outer i is relevant inside the loop. In those cases, shadowing is invisible in practice, because no one needs the outer variable inside the inner scope, and the inner name is the right one to use there. The problem arises only when code inside the inner scope intends to use the outer variable but accidentally declares an inner one with the same name, or when a reader cannot tell which variable a use refers to without careful inspection.

The remedy is partly naming discipline and partly awareness. When an inner scope needs a different value from an outer variable of the same name, give the inner variable a distinct, more specific name, so that the difference is visible and the outer variable remains accessible if needed. When an inner scope deliberately shadows, a comment noting that the inner name is intentional can help future readers, though in most cases the structure of the code makes the intention clear. And when debugging a variable that holds an unexpected value, check for shadowing: is there an inner declaration with the same name that is hiding the outer variable you expected? That check — searching the enclosing scopes for a same-named declaration — is a quick step that resolves a class of bugs where the value is "wrong" but the logic looks right, because the logic is operating on a different variable from the one the debugger assumed.

Shadowing also illustrates the nesting rule from Section 16 in its sharpest form: visibility is determined by the nearest enclosing declaration. When a name is used, JavaScript looks for its declaration in the current scope first, then in each enclosing scope in turn, and the first declaration found wins. That nearest-declaration rule is the scope chain, which the next section explores, and shadowing is simply what happens when the nearest declaration is not the outermost one.

Check: What happens to an outer variable when an inner scope declares the same name?

Section 22 — The scope chain and lookup

When code uses a variable, JavaScript must find that variable's declaration, and it does so by following the scope chain: it looks in the current scope first, and if the name is not declared there, it looks in the enclosing scope, then in the next enclosing scope, and so on outward, until it reaches the global scope. If the name is found at any level, that declaration is the one used. If it is not found at any level, the use throws a ReferenceError, because the name is not defined anywhere visible. That chain — current scope outward to global — is the mechanism behind every variable lookup, and understanding it explains shadowing, explains why inner code can see outer variables, and explains why outer code cannot see inner ones: the chain only goes outward, never inward.

The scope chain is determined by where functions and blocks are written, not by where they are called, because JavaScript uses lexical scope, as Section 16 explained. A function defined inside another function can see the outer function's variables, even when it is called from elsewhere, because its scope chain was fixed at definition time to include the outer function's scope. That property is what makes closures possible, as the next sections explore: a function carries its defining scope with it, and can use variables from that scope long after the outer function has returned. Without lexical scoping, closures would not work, and functions would be far less powerful as units of organisation.

Lookup has a performance and a readability implication that is worth noting, though it rarely matters in practice. A variable found in the current scope is found immediately, while a variable found several levels out requires walking the chain, but that walk is fast and is not a reason to avoid outer variables. The readability implication matters more: a variable used deep inside nested scopes, whose declaration is several levels out, requires the reader to walk the same chain to understand it, and the further the declaration, the harder the reading. That is why the smallest-sufficient-scope discipline from Section 19 helps: variables declared close to their uses are easier to find, and code that relies heavily on distant globals is harder to follow than code that passes values as parameters, where the input is visible in the function's signature.

There is also a failure mode of lookup that is worth recognising. When a name is not found anywhere in the chain, the error message names the missing variable, and the debugging steps are to check spelling, to check whether the declaration is in a scope that encloses the use, and to check whether the declaration runs before the use in execution order. Each of those checks corresponds to a link in the chain: spelling determines whether the name matches a declaration, enclosure determines whether the declaration is on the chain, and order determines whether the declaration has been reached when the use executes. A ReferenceError is therefore not just an error; it is a report about the scope chain, saying that the chain, as constructed from the code's nesting and as executed up to that point, contains no such name. Reading it that way turns the error into a precise diagnostic.

Check: In which order does JavaScript search scopes for a variable's declaration?

Section 23 — Closures: functions that remember

A closure is a function that remembers the variables from the scope where it was defined, even after that scope has finished executing. When an inner function is created inside an outer function, the inner function carries a reference to the outer function's scope, and it can use the outer function's variables long after the outer function has returned. That remembered scope is the closure, and it is one of JavaScript's most distinctive and powerful features. It means a function is not just its code; it is its code plus the environment it was born in, and that environment travels with it wherever the function goes.

The classic example is a function that creates and returns another function. The outer function declares a variable, defines an inner function that uses that variable, and returns the inner function. After the outer function returns, its local variables would normally be discarded, but because the returned inner function still refers to them, they are kept alive, accessible only through that inner function. That kept-alive state is private: no other code can reach those variables except through the returned function, which makes closures a mechanism for privacy and for state that persists between calls without being global. A counter built this way — an outer function holding a count, an inner function incrementing and returning it — keeps its count private and persistent, without polluting the global scope.

function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}
const counter = makeCounter();
counter(); // 1
counter(); // 2

Closures appear constantly in real front-end code, often without being named as such. An event handler defined inside a function that sets up a page element remembers the element and the data from that setup, even though the setup function finished long ago. A callback passed to a timer or a data-fetching operation remembers the variables from the code that started the operation, even though that code has moved on. A function returned from a configuration step remembers its configuration. In each case, the function plus its remembered scope is doing work that would otherwise require global variables or explicit state-passing, and doing it more cleanly, because the state is attached to exactly the function that needs it and to no other code.

Understanding closures requires letting go of the simple lifecycle from Section 18 — local variables die when the function returns — and replacing it with a refined version: local variables die when the function returns, unless a surviving function still refers to them, in which case they live as long as that function does. That refinement is the whole of closures, and it is less mysterious than it first sounds: the language keeps what is still needed and discards what is not, and a returned function is a need. The next section works through a concrete closure example in detail, because closures are best learned by watching one work, step by step, rather than by definition alone.

Check: What is a closure, in one sentence?

Section 24 — A closure example: the counter

The counter is the canonical closure example because it is small, concrete, and shows every part of the mechanism. An outer function, makeCounter, declares a local variable count starting at 0, defines an inner function that increments count and returns its new value, and returns that inner function. Calling makeCounter creates a fresh count and a fresh inner function bound to it, and storing the returned function in a variable, counter, keeps that count alive. Each call to counter increments the same count and returns its new value: the first call gives 1, the second gives 2, the third gives 3, and so on. The count is not global — no other code can see or change it — and it is not reset on each call, because it lives in the closure, not in the inner function's own locals, which are fresh on each call.

What makes the example instructive is comparing it with the alternatives. A global count would work, but it would be visible and changeable by all code, and a second counter would need a second global, with names proliferating. A count stored in the inner function as a local would reset to its starting value on each call, because locals are fresh each time, and the counter would never count past 1. The closure is the solution that is both private and persistent: private, because only the returned function can reach the count, and persistent, because the count survives between calls. That combination — privacy plus persistence without globals — is the closure's distinctive value, and it is why closures are used for state that should be encapsulated: counters, toggles, caches, and configuration that should not be exposed.

Two counters created from the same outer function are independent, because each call to makeCounter creates a fresh scope with its own count. Calling makeCounter twice gives two inner functions, each bound to its own count, and incrementing one does not affect the other. That independence is important, because it means closures are not shared state by default; each closure captures its own environment, and environments created by separate calls are separate. A program can therefore create many independent counters, each with its own private count, from a single factory function, which is a pattern — a function that creates and returns specialised functions — that appears throughout JavaScript, from event handlers to data processing.

There is a practical debugging note for closures. When a closure behaves unexpectedly, the variable to inspect is the captured one, and the question to ask is which invocation's scope it belongs to. A closure created in a loop, for example, may capture a variable shared across iterations rather than a fresh one per iteration, which is the subject of Section 45, and the fix is to ensure each closure gets its own scope, usually by declaring the captured variable with let inside the loop body. That debugging step — identifying the captured variable and its scope — is the closure-specific version of the general scope question from Section 16: where was this variable declared, and which invocation's box does this function remember? Answering that question resolves most closure bugs.

Check: Why does the counter keep counting instead of resetting on each call?

Section 25 — Hoisting with var

Hoisting is the behaviour where declarations are conceptually moved to the top of their scope before code runs, so that they are available throughout the scope even before the line where they are written. For var, hoisting means a variable declared anywhere in a function is treated as if it were declared at the top of that function, though its assignment stays where it was written. Using a var variable before its declaration line therefore does not throw an error; it gives undefined, because the declaration has been hoisted but the assignment has not yet run. That split — declaration hoisted, assignment not — is the source of var's confusing behaviour, where a variable exists but holds undefined in a region where a reader would expect it not to exist at all.

console.log(message); // undefined, not an error
var message = "hello";

Hoisting also applies to function declarations, which are hoisted completely, including their bodies, so that a function declaration can be called before its line in the file, as Section 26 explains. The two cases differ in an important way: a hoisted function is fully usable before its line, while a hoisted var is declared but unassigned before its line, holding undefined. That difference reflects the nature of the two declarations: a function declaration is a complete definition, while a var declaration is a name plus a separate assignment, and only the name is hoisted. Understanding that difference prevents the common confusion of treating var hoisting as if it made the value available early; it makes only the name available, and the value arrives at the assignment line, as usual.

The practical advice for var hoisting is the same as for var generally: do not rely on it, and do not write new code with var. But when reading old code, hoisting explains behaviour that otherwise looks impossible: a variable used before its declaration line without an error, a function called before its definition without an error, and a variable that is undefined in a region where it seems it should not exist. Those behaviours are not bugs in the language's execution; they are hoisting working as designed, and recognising them as hoisting turns a puzzling reading into an understood one. The modern declarations, let and const, replace hoisting's silent undefined with the temporal dead zone error, which is stricter and clearer, as the next sections explain, but the concept of hoisting remains essential for reading the large body of existing JavaScript written with var and function declarations. Hoisting is also why reading old code requires a different mental model from writing new code. When you write with let and const, the order of the text matches the order of availability, but when you read var code, you must mentally lift declarations to the top of their function before judging whether a use is valid. That mental lifting is a skill worth practising on small examples, because it makes old codebases far less mysterious.

Check: What value does a var variable hold when used before its declaration line?

Section 26 — Hoisting function declarations

Function declarations are hoisted completely: both the name and the body are moved, conceptually, to the top of their scope before any code runs, so a function declaration can be called before the line where it is written, and it will work exactly as if it had been defined earlier. That behaviour allows a style of organisation where the high-level flow of a program appears first — the calls that describe what the program does — and the detailed function definitions follow below, like footnotes to the main text. A reader meets the story first and the details second, which is often the most natural order for understanding a program, and it is an order that hoisting makes possible without any special syntax.

greet(); // works, even though greet is defined below
function greet() {
  console.log("Hello!");
}

Complete hoisting distinguishes function declarations from function expressions and arrow functions, which are not hoisted in the same way. A function expression stored in a const or let variable is subject to that variable's temporal dead zone: calling it before its declaration line throws an error, because the variable has not yet been initialised, even though the function value exists in the code text below. An arrow function stored in a variable behaves the same way. That difference is a practical reason to choose declarations for functions that are called from many places, including from code above their definitions, and to choose expressions when the function's availability should begin at its definition line, which is often the case for callbacks and for functions created conditionally. The choice is not about which form is better in the abstract; it is about when the function should become available, and hoisting is the mechanism that decides that for declarations.

There is a nuance about hoisting within blocks that is worth knowing, though it rarely affects everyday code. In modern JavaScript, a function declaration inside a block is scoped to that block, and its hoisting is confined to the block, which is a stricter and more predictable rule than the older behaviour, where block-level function declarations were effectively function-scoped in some environments. That refinement is part of the language's gradual tightening, and it aligns block-level functions with the block-scope rules for let and const. For functions declared at the top level of a script or at the top level of another function, the rule is simple and stable: the function is available throughout its scope, before and after its line.

The practical guidance is to use hoisting deliberately rather than accidentally. If your codebase organises code with the main flow first and helpers below, function declarations support that organisation cleanly, and you can rely on hoisting as a feature. If your codebase defines functions before their uses, hoisting is irrelevant, and either form works. What should be avoided is relying on hoisting without realising it — calling a function before its definition and being surprised that it works, or worse, restructuring code and being surprised that a call that used to work now fails because the function was changed from a declaration to an expression.

Check: Can you call a function declaration before its definition line?

Section 27 — The temporal dead zone

The temporal dead zone is the region of code, in execution order, between the start of a scope and the declaration line of a let or const variable within that scope, during which the variable exists but cannot be accessed. Using the variable in that region throws a ReferenceError with a message about accessing the variable before initialisation, which is the language's way of refusing to supply a value that has not yet been assigned. The zone is called temporal because it is about time — the period before the declaration executes — rather than about place, and it is called dead because the variable is unusable during it, even though its name is known to the scope. That combination — known but unusable — is what distinguishes the dead zone from a variable that does not exist at all, which would also throw a ReferenceError but with a "not defined" message.

The dead zone exists as a deliberate improvement over var's hoisting, where using a variable before its declaration gave undefined silently. Silent undefined allowed programs to proceed with a missing value and to fail later, far from the cause, with a confusing symptom. The dead zone error, by contrast, fails loudly at the point of the mistake, naming the variable and the problem, which makes the fix obvious: move the use after the declaration, or move the declaration before the use, in execution order. That loudness is a design choice that trades a small amount of flexibility — the ability to use a variable before its line — for a large amount of debuggability, and it is a trade that modern JavaScript makes consistently: prefer an immediate, clear error over a silent, confusing wrong value.

The dead zone also applies to function expressions and arrow functions stored in let or const variables, because those variables are subject to the same rule: the variable cannot be accessed before its declaration line, so a function expression cannot be called before its line, even though the function definition appears in the code below. It applies to class declarations as well, which are also block-scoped and dead-zoned, a detail for the objects chapters. It does not apply to function declarations, which are fully hoisted, as the previous section explained, nor to var variables, which are hoisted with undefined. That contrast — declarations that hoist fully, variables that dead-zone, var that hoists partially — is the complete hoisting picture, and holding those three cases distinct is the key to predicting whether a use before a declaration will work, throw, or give undefined.

In practice, the dead zone is encountered most often in two situations: a variable used at the top of a file before a declaration lower down, and a function called during initialisation that reads a variable declared later in the initialisation sequence. Both are ordering problems, and both are fixed by reordering so that declarations execute before their uses. The deeper habit is to declare variables close to, and before, their first use, which is also good readability practice, because a reader meets a variable's birth before meeting its uses. The dead zone enforces that habit with an error, which is the language helping you write code that reads in order.

Check: What error do you get from using a let variable in its temporal dead zone?

Section 28 — Immediately invoked function expressions

An immediately invoked function expression, usually shortened to IIFE, is a function expression defined and called in the same step, by wrapping it in brackets and adding a call bracket pair immediately after. The form (function() { /* code */ })(); defines an anonymous function and runs it at once, and its variables live inside the function's scope, invisible outside. Before modules became standard, IIFEs were the primary mechanism for avoiding global pollution: wrapping a script's code in an IIFE kept all of its variables out of the global scope, so that several scripts on a page could each have their own variables without clashing. That historical role has largely been taken over by modules, but IIFEs remain in old code and in specific modern uses, and understanding them is part of reading JavaScript fluently.

(function () {
  let privateValue = 42;
  console.log(privateValue);
})();
// privateValue is not visible here

The syntax repays a moment of attention, because it looks unusual. The outer brackets around the function turn it from a declaration, which cannot be immediately called, into an expression, which can. The trailing brackets are the call operator, running the expression at once. Without the outer brackets, a function keyword at the start of a statement is read as a declaration, and adding call brackets after a declaration is a syntax error, because declarations are not expressions to be called. With the brackets, the function is an expression value, and calling it is natural. Arrow functions can be used the same way: (() => { /* code */ })(); is an immediately invoked arrow function, with the same scoping effect and a more modern appearance.

Modern uses of IIFEs are narrower than their historical use, but they persist. An IIFE is useful when a piece of code needs its own scope for a one-off task, such as initialising a component with temporary variables that should not leak, or creating a closure in a loop in older code before let made that unnecessary. An IIFE is also used in asynchronous patterns, where an async function must be invoked immediately to start its work, though modules with top-level await have reduced that need. In each case, the IIFE's value is its scope: it creates a box, runs code inside it, and discards the box, leaving no trace in the surrounding scope except for whatever the code deliberately exposed, usually by assigning to an outer variable or by returning a value.

The practical guidance is to recognise IIFEs when reading old code, to understand that their purpose is scoping rather than reuse — an IIFE runs once and is gone, unlike a named function that can be called repeatedly — and to prefer modules for new code's scoping needs, since modules provide the same isolation with a clearer syntax and with import and export mechanisms for sharing. An IIFE in new code should have a specific reason, such as a one-off initialisation that must not leak variables in a non-module script, and that reason should be clear from context. Like var, IIFEs are a historical tool that remains valid and that you will meet in real code, and like var, they are best understood as a solution to a problem that newer features solve more cleanly.

Check: What is an IIFE, and why was it historically useful?

Section 29 — Callbacks

A callback is a function passed as an argument to another function, to be called later, when an event happens or a task completes. The name comes from the idea that the receiving function will "call back" to the supplied function at the appropriate moment, rather than the supplier calling it directly. Callbacks are how JavaScript handles things that do not happen immediately: a button click, a timer finishing, data arriving from a server, an animation completing. The program registers interest by passing a function, and the system invokes that function when the moment arrives, with relevant values as arguments. Without callbacks, a program could only do things in a fixed sequence, and interactive, event-driven behaviour would be impossible.

function greetLater(callback) {
  callback("Ada");
}
greetLater((name) => console.log(`Hello, ${name}!`));

Callbacks are a direct consequence of functions being values, as Section 8 introduced: because a function can be stored and passed like any value, it can be handed to another function for later use. The receiving function decides when and with what arguments to call it, and the callback decides what happens at that moment. That division of labour is powerful: a timing function knows when to call but not what to do, and a callback knows what to do but not when, and together they produce timed behaviour without either needing to know the other's details. The same pattern applies to events: the browser knows when a click happens, and the callback knows what the click should do, and the event system connects them.

Callbacks introduce a style of thinking that takes practice: code that runs later, not now. When you pass a callback, the lines after the registration do not wait for the callback; they run immediately, and the callback runs later, when its trigger occurs. That asynchrony is the source of a classic beginner confusion: a value set inside a callback is not available immediately after the registration, because the callback has not run yet, and code that reads the value too early finds it still empty. The remedy is to put the code that needs the callback's result inside the callback, or in a function the callback calls, so that it runs after the result exists. That "put it in the callback" discipline is the basic form of asynchronous programming, and later chapters on promises and async functions build more elaborate structures on the same idea, but the core insight — later code belongs inside the later-running function — remains unchanged.

Callbacks can also nest, when one callback's work triggers another asynchronous task with its own callback, and deeply nested callbacks become hard to read, a situation once nicknamed "callback hell" because of the staircase of indentation it produces. Modern JavaScript offers promises and async functions to flatten that staircase, and later chapters cover them, but callbacks remain fundamental: promises are built on callbacks, event handlers are callbacks, and many libraries still accept callbacks directly. Learning callbacks well — what they are, when they run, and how to place dependent code inside them — is therefore not a detour but a foundation, and every asynchronous pattern you meet later will be easier for having understood this one first.

Check: What is a callback, and when does it run?

Section 30 — Functions as values

Functions are values in JavaScript, in the fullest sense: they can be assigned to variables, stored in arrays and objects, passed as arguments, and returned from other functions, just like numbers and strings. That statement, introduced with function expressions in Section 8, deserves a section of its own, because it is the gateway to the most expressive patterns in the language. When functions are values, a program can treat behaviour as data: it can choose which function to run based on a condition, store functions for later use, and build new functions from old ones. A list of validation functions, each checking one rule, can be stored and run in turn. A mapping from event names to handler functions can dispatch the right handler for each event. A function that returns a specialised function, like the counter factory in Section 24, creates behaviour on demand.

The practical consequences appear in everyday code. Storing a function in a variable, as with const greet = function() {}, makes the function available under that name and subject to that variable's scope and reassignment rules. Passing a function as an argument, as with callbacks in Section 29, lets the receiver decide when to run it. Returning a function from a function, as with closures, lets the returned function carry its defining scope with it. Storing functions in an object, with names as keys, creates a dispatch table: handlers.click runs the click handler, handlers.submit runs the submit handler, and the program can select behaviour by name rather than by a chain of conditions. Each of these is the same idea — a function treated as a value — applied in a different container.

Functions as values also enable higher-order functions, which Section 31 covers: functions that take other functions as arguments or return them as results. Higher-order functions are the mechanism behind the array methods that later chapters explore, behind event systems, behind middleware and plugins, and behind much of modern JavaScript's expressiveness. The idea that a function can operate on functions, not just on numbers and strings, is what lifts JavaScript from a language of fixed procedures to a language of composable behaviour, where small functions are combined into larger ones by passing and returning them.

There is a readability discipline that comes with functions as values. When a function is passed or stored, its name — or its lack of one, for anonymous functions — affects how understandable the code is. A named function passed as an argument, like processData(validate), reads clearly: the argument is the validate function, and its purpose is visible. An anonymous function passed inline, like processData((x) => x * 2), is clear when it is short, because its body is visible at the call site, but it becomes opaque when it is long, because the call site is cluttered with a function body that belongs elsewhere. The guideline is to inline short functions and to name and define longer ones separately, passing them by name, so that call sites stay readable and function bodies stay findable. Functions as values give you freedom; naming gives that freedom structure.

Check: In what ways can a function be used like any other value?

Section 31 — Higher-order functions

A higher-order function is a function that takes another function as an argument, or returns a function as its result, or both. The name comes from mathematics, where a higher-order function operates on functions rather than on ordinary values, and the concept is central to JavaScript's style. A function that takes a callback, like the event and timer functions from Section 29, is higher-order, because it operates on a function supplied by its caller. A function that returns a specialised function, like the counter factory from Section 24, is higher-order, because its result is a function. A function that does both — taking a function and returning a new function built from it — is the most powerful form, and it appears in patterns like wrapping a function with additional behaviour, such as logging or validation, without changing the original.

A concrete example clarifies the idea without requiring any new syntax. Consider a function called applyTwice, which takes a function and a value, applies the function to the value, and then applies it again to the result. Calling applyTwice(double, 5) gives 20, because doubling 5 gives 10, and doubling 10 gives 20. The function applyTwice does not know what double does; it only knows how to apply a function twice, and that generality is the point. The same applyTwice works with any function: tripling twice, greeting twice, or any other operation supplied by the caller. Higher-order functions separate the pattern — apply twice, run when ready, repeat until done — from the specific operation, so that the pattern is written once and reused with many operations, which is the same economy that functions generally provide, lifted one level up.

Higher-order functions are also the mechanism behind the array methods that a later chapter covers in detail. Methods that transform each item of a list, that filter a list by a condition, or that combine a list into a single value all take a function as an argument — the transformation, the condition, or the combination — and apply it to the list's items according to their own pattern. Those methods are higher-order functions attached to arrays, and understanding higher-order functions in general makes those methods intuitive when they arrive: each method is a pattern, and the argument you supply is the specific operation for that pattern. That framing — pattern plus operation — is a useful way to read any higher-order function: ask what pattern it implements, and ask what operation the caller supplies to fill the pattern's variable part. That pattern-plus-operation framing also explains why higher-order functions feel reusable: the pattern stays fixed while the operation varies.

That separation is also what makes higher-order functions easy to test: test the pattern once, then test each operation on its own.

Check: What makes a function higher-order?

Section 32 — Anonymous functions

An anonymous function is a function without a name of its own. It is defined as an expression — as an argument to another function, as a value assigned to a variable that gives it an inferred name, or as a value in a list or object — and it is used where it is defined, without being referred to by a name elsewhere. Anonymous functions are common in modern JavaScript, particularly as callbacks, where the function is used exactly once, at the point of registration, and a name would add little: a click handler written inline, a short transformation passed to a list method, a timer callback that runs once. In those cases, anonymity is a virtue, because it keeps the function close to its use, and a reader can see what it does without jumping to a separate definition.

setTimeout(function () {
  console.log("Time is up!");
}, 1000);

Anonymity has a cost in debugging. When an error occurs inside an anonymous function, the stack trace may show the function as anonymous, making it harder to identify which function failed, particularly when several anonymous functions are in play. Modern tools mitigate this by inferring a name from context — a function assigned to a variable takes that variable's name in traces, and a function passed as an argument may take a name from its position — but the mitigation is not perfect, and a long anonymous function in a complex callback chain can still be hard to trace. The remedy is proportionate: short anonymous functions are fine, because their context identifies them, while long or reused anonymous functions should be given names, either as declarations or as named function expressions, so that traces and readers can identify them.

There is also a readability cost to long anonymous functions that goes beyond debugging. A call site cluttered with a multi-line anonymous function body is hard to scan, because the call's purpose — what is being registered, what is being transformed — is buried inside the function's implementation. Extracting the function to a named definition above the call, and passing it by name, restores the call site's readability: the call says what happens, and the definition, found by its name, says how. That extraction is the same refactoring as Section 48's, applied to anonymous functions, and it is a judgement call: inline when the function is short and its purpose is obvious from context, extract when it is long or when its purpose needs a name to be clear.

Anonymous functions are not a separate kind of function; they are ordinary functions without a name, and they follow the same rules for parameters, return values, scope and closures as named functions. Arrow functions are frequently anonymous, which is part of their appeal for short callbacks, but an arrow function assigned to a variable gains an inferred name, and a function expression can be given an explicit name even when assigned, as Section 8 explained. The choice between anonymous and named is therefore a choice about readability and debuggability, not about capability, and it should be made case by case, with the guideline that a function deserves a name when it is long, reused, or likely to appear in a stack trace that someone will need to read.

Check: When is an anonymous function a good choice, and when should a function be named?

Section 33 — Naming functions well

Function names are the primary way a program communicates what it does, because function calls are the sentences of a program's high-level flow. A well-named function makes its call site read like a description: calculateTotal(price, tax) says what happens, validateEmail(address) says what is checked, displayWelcomeMessage(name) says what the user will see. A poorly named function makes its call site a puzzle: doStuff(data) says nothing, process(input) says almost nothing, and handleThing(x) says nothing while pretending to say something. The difference is not cosmetic; it determines whether a program can be understood by reading its calls, or whether every call requires jumping to the definition to learn what it does. Good names make the high level readable; bad names force every reader to the low level.

Verb-first names are the convention, because functions do things, and the verb should be specific. Calculate is better than get when a computation happens, because get suggests a simple retrieval. Validate is better than check when a rule is being enforced, because check is vague about what happens on failure. Fetch is better than load when data comes from a network, because load could mean many things. Find is better than get when a search is involved, because find signals that the result might not exist. The specific verb tells the reader what kind of work the function does, and the object that follows the verb tells them what it works on: calculateTotal, validateEmail, fetchUserData, findProductById. That verb-plus-object pattern produces names that are both precise and predictable, so that a reader can often guess a function's name before searching for it.

There are also conventions for functions that answer questions. A function returning a boolean is often named as a question or a state, starting with is, has, can, or should: isValid, hasItems, canProceed, shouldShow. That naming makes conditions read like sentences: if (isValid(email)) reads as "if the email is valid", which is the condition's meaning made plain. A function returning a value is named for the value: getUserName, calculateTotal, formatDate. A function performing an action is named for the action: displayMessage, saveData, sendRequest. Following those three patterns — question names for booleans, value names for getters and calculators, action names for effects — makes a codebase's functions predictable, so that a reader can infer a function's kind from its name before reading its body.

Finally, a function's name should reflect its single job, and a name that needs "and" in it — saveAndDisplay, fetchAndValidateAndStore — is a signal that the function does several jobs and should probably be split, with each job named separately and orchestrated by a caller. That signal is valuable, because it connects naming to design: if you cannot name a function without "and", the function is likely too broad, and splitting it will improve both the name and the code. Naming well is therefore not a finishing touch applied after code is written; it is a design activity that reveals structure, and a function that is hard to name is often a function that needs redesigning. Spend time on names, because names are read far more often than bodies, and a good name is the cheapest documentation a program can have.

Check: Why should function names usually start with a verb?

Section 34 — One job per function

The principle that each function should do one job is the most important design guideline in this chapter, and it is worth stating precisely: a function should have a single, coherent responsibility, which can be described in one sentence without using "and" to join unrelated tasks. A function that calculates a total does that job. A function that calculates a total and displays it and saves it does three jobs, and it should be three functions — calculate, display, save — orchestrated by a caller that does each in turn. The single-job function is easier to understand, because its body is about one idea. It is easier to test, because its inputs and outputs concern one idea. It is easier to reuse, because a caller that needs only the calculation can call the calculation without also triggering the display and the save. And it is easier to fix, because a bug in one job lives in one function, not tangled with other jobs.

The difficulty is recognising when a function has more than one job, because jobs can hide inside apparently simple code. A function that fetches data, filters it, formats it, and displays it has four jobs, even though it reads as a single flow. A function that validates a form and shows error messages has two jobs, validation and display, which should be separate so that validation can be tested without a page and display can be changed without touching validation. The test to apply is the "and" test from the previous section: describe the function in one sentence, and if the sentence needs "and" to join distinct responsibilities, the function needs splitting. Another test is reuse: if a caller would want only part of the function, that part should be its own function. A third test is testing: if testing the function requires setting up several unrelated concerns, those concerns are separate jobs.

Splitting a function is not always about making functions tiny. A function can be long and still have one job, if its job is inherently involved — a complex calculation with several steps is one job, as long as the steps all serve that calculation. And a function can be short and still have two jobs, if its few lines do two unrelated things. Length is not the measure; coherence is. The measure is whether the function's steps all serve a single purpose that a reader can hold in mind as one idea. When they do, the function is well-factored regardless of length. When they do not, the function should be split, with each resulting function named for its job and the original function becoming, if needed, an orchestrator that calls the pieces in order.

Check: How can you tell whether a function is doing more than one job?

Section 35 — Parameters instead of globals

A function that reads a value from a global variable, rather than receiving it as a parameter, hides its true inputs. Its signature says it needs nothing, but its body depends on a value that lives elsewhere, and that value can change between calls for reasons invisible in the function. Such a function is harder to test, because a test must set up the global before calling it, and harder to reuse, because a new program that wants the function must also provide the global, with the right name and the right value. It is also harder to reason about, because a reader cannot tell from the signature what the function depends on, and must read the whole body to find its hidden inputs. Parameters solve all of this by making inputs explicit: a function that takes its inputs as parameters declares, in its signature, everything it needs, and nothing else.

function greet(name) {
  return `Hello, ${name}!`;
}

A function greet(name) can be tested by calling it with "Ada", "Ben", or any other value, and checking the result, with no setup. A function greet() that reads a global userName can only be tested by setting userName first, then calling it, then checking the result, and then resetting userName for the next test, so that tests do not interfere with each other. That setup is not just extra work; it is a source of test interference, where one test's leftover global value affects another test's result, producing failures that depend on test order. Parameter-based functions avoid that entire class of problem, because each call carries its own inputs and leaves no trace.

There are legitimate exceptions, and they are worth naming so the rule does not become rigid. Truly program-wide constants — a tax rate, a configuration value that never changes — can be read as globals without much harm, because they do not change and therefore do not create order-dependence, though passing them as parameters is still clearer in many cases. Values that are genuinely ambient, like the current page's document, are globals by necessity, because they belong to the environment rather than to the program's logic. But values that vary — user input, calculated totals, fetched data, application state — should travel as parameters and return values, not as globals, because their variation is exactly what makes hidden dependence dangerous. The test is whether the value changes during the program's life: if it does, pass it; if it never does, a global constant is acceptable, though a parameter is still often clearer.

The habit to form is to write functions that are explicit about their needs. When you catch yourself reaching for a global inside a function, pause and ask whether that value should be a parameter instead. In most cases it should, and adding it to the parameter list is a small change that makes the function honest, testable, and reusable. Over a program, that honesty compounds: functions with clear parameter lists compose into programs whose data flow is visible, because values travel through calls rather than through shared state, and a program whose data flow is visible is a program that can be understood, tested, and fixed with confidence.

Check: Why should a function take its inputs as parameters rather than reading globals?

Section 36 — Recursion

Recursion is when a function calls itself, directly or through other functions, to solve a problem by breaking it into a smaller version of the same problem. A recursive function has two parts: a base case, which is a simple situation it can answer directly without calling itself, and a recursive case, which reduces the problem towards the base case and calls itself on the smaller problem. The classic example is counting down: a function that prints a number and then calls itself with the number minus one, stopping when the number reaches zero. The base case is zero, where it stops. The recursive case is any larger number, where it prints and recurses. Each call works on a smaller problem, so the calls must eventually reach the base case, and the recursion ends.

function countDown(n) {
  if (n <= 0) {
    return;
  }
  console.log(n);
  countDown(n - 1);
}
countDown(3); // prints 3, 2, 1

Recursion is natural for problems that are defined in terms of themselves. A folder contains files and other folders, each of which contains files and other folders, so listing all files in a folder is naturally recursive: list the files here, and for each subfolder, list its files recursively. A mathematical sequence defined by its previous terms is naturally recursive. A tree-shaped structure, like the DOM or a family tree, is naturally traversed recursively, because each branch is a smaller tree. In those cases, a recursive solution mirrors the problem's structure, and it is often shorter and clearer than an iterative solution that must manage its own stack of pending work, because the language's call stack does that management for free.

The danger of recursion is a missing or unreachable base case, which produces infinite recursion: the function calls itself forever, each call adding a frame to the call stack, until the stack overflows and the browser stops the program with a "Maximum call stack size exceeded" error, which Section 19 of the debugging chapter mentioned. That error is the signature of runaway recursion, and its stack trace shows the same function repeated many times, which is a clear diagnostic. The remedy is to verify the base case before anything else: does the recursive case always move towards the base case, and is the base case reachable from every starting input? A countdown that subtracts one reaches zero from any positive starting number. A countdown that subtracts zero, or that adds instead of subtracting, never reaches zero, and will overflow. Checking that progression — each recursive call strictly closer to the base case — is the essential safety check for any recursive function. Checking that progression before running the code is a habit that prevents the most dramatic recursion failures.

A base case that is never reached is not a base case at all, merely a hope written in code.

Check: What two parts must a recursive function have?

Section 37 — Keeping functions readable

A function's readability is determined less by its length than by its structure: clear names, a single job, short steps, and a flow that a reader can follow without holding too much in mind at once. Several concrete habits produce that readability. The first is to keep functions short enough to see at a glance, as a rough guide, a function that fits on one screen without scrolling is easier to understand than one that sprawls, because the reader can hold its whole shape in view. When a function grows beyond that, it is often a sign that it contains a sub-job that should be extracted into its own function, named for that job, so that the original function becomes a readable sequence of calls to well-named helpers. Extraction is the primary tool for readability, and it should be used whenever a section of a function can be named as a job of its own.

The second habit is to prefer straightforward flow over clever flow. A function that reads top to bottom, with guard clauses handling edge cases early and the main logic unnested, is easier to follow than a function with deeply nested conditions, where the reader must track several levels of "if" to know which code runs when. Early returns, from Section 13, flatten that nesting. Meaningful variable names, from the previous chapter, make each step self-explanatory. Comments that explain why, from the previous chapter's comments section, cover the decisions that code alone cannot express. Together, those habits produce functions that read like a description of their job rather than like a puzzle to be decoded.

The third habit is consistency. Functions in a codebase should follow the same conventions: similar jobs should be structured similarly, similar names should follow the same pattern, and similar error handling should look the same. Consistency lets a reader transfer understanding from one function to another, so that the tenth function in a file is easier to read than the first, because its shape is familiar. Inconsistency forces a reader to learn each function from scratch, which is exhausting and error-prone. That is why team style guides exist, and why a solo developer benefits from a personal style: a consistent style is a form of communication, telling readers what to expect before they read.

Finally, readability is tested by reading. After writing a function, read it as if you had never seen it, following its flow, checking its names, and asking whether each step's purpose is clear. If you must pause to work out what a line does, that line needs a better name, a comment, or a restructuring. If you must scroll to understand the function, it needs splitting. That self-review, done routinely, is the cheapest quality control available, and it catches problems while they are still easy to fix, in your own code, rather than later, in a review or in a bug report. Readable functions are not a luxury; they are the form in which maintainable programs exist.

Check: What is the primary tool for making a long function readable?

Section 38 — Debugging functions

Debugging a function starts with the same questions as debugging any code, applied to the function's specific shape: did the function run, what arguments did it receive, what did it return, and where inside it did the value go wrong? The first question is answered by a log or a breakpoint on the function's first line: if the log appears or the breakpoint pauses, the function ran; if not, the problem is in the caller or in the event that should have triggered the call, not in the function itself. That binary test — ran or did not run — eliminates half the possible causes in one step, and it should always be the first step when a function seems not to work, before any inspection of its body.

The second question, about arguments, is answered by logging the parameters at the function's start, or by inspecting them in the Scope pane at a breakpoint. Arguments that are wrong — missing, swapped, or of the wrong type — produce wrong results from a correct body, and checking arguments first prevents the waste of debugging a body that is innocent. A function that returns "Hello, undefined!" is not broken in its greeting logic; it was called without a name, or with a name that was undefined at the call site, and the fix belongs at the call, not in the function. That pattern — correct body, wrong inputs — is common enough that checking inputs before body should be a reflex.

The third question, about where inside the function a value goes wrong, is answered by stepping through the function with breakpoints, watching its local variables in the Scope pane, and comparing each intermediate value with expectation. A function that calculates a total incorrectly will show, on stepping, the line where the total first diverges from the expected value, and that line is the neighbourhood of the bug, whether it is a wrong operator, a wrong variable, or a wrong order of operations. Stepping is particularly valuable inside functions because functions are self-contained: their locals are visible in the Scope pane, their parameters are visible as inputs, and their return value is visible at the exit, so the whole computation can be watched from input to output in a confined space.

A final debugging habit for functions is to test them in isolation. A function that can be called from the Console with known arguments and whose result can be compared with an expected value is a function that can be debugged without running the whole program. That isolation is one more benefit of pure functions and parameter-based inputs: they can be exercised directly, with controlled inputs, which makes their bugs reproducible on demand. When a function is hard to test in isolation — because it depends on globals, on the page, or on other functions that are hard to set up — that difficulty is itself a design signal, suggesting the function should be refactored towards purity and parameters. Debugging and design meet here: functions that are easy to debug are usually functions that are well designed, and the effort spent making a function testable pays for itself the first time it breaks.

Check: What should you check first when a function seems not to work?

Section 39 — Reading a stack trace

When a function throws an error, the Console prints a stack trace: a list of the function calls that were active at the moment of the error, with the failing function on top and its callers below, each with a file and line number. Reading a stack trace is the skill of following that list from the failure back to its origin, and it is essential because the failing function is often not the function that caused the problem: it is the function where a wrong value finally caused a crash, while the function that supplied the wrong value sits further down the stack. The trace gives you the whole chain, and your job is to walk it until you find the link where the wrongness entered.

Start at the top, where the error occurred, and read the failing line: what operation failed, and what value was involved? Then move to the next frame, which is the function that called the failing function, and ask what arguments it passed: were they correct, and if not, where did they come from? Continue down the stack, frame by frame, checking each caller's arguments and state, until you reach a frame where a value first went wrong — a miscalculated argument, a missing value, a wrong branch taken. That frame is the neighbourhood of the fix, even though the error surfaced several frames above it. Frames from library or framework code can usually be skipped, because the library is reacting to your code's input, but do not skip them blindly: occasionally the library frame reveals that your call used its interface incorrectly, which is itself the fix.

Stack traces also appear in the debugging chapter's Call Stack pane when paused at a breakpoint, and the reading skill is identical: top is current, bottom is origin, and walking down is walking back in time through the program's decisions. Practising that walk on real errors — taking a trace, clicking each frame, reading each caller's code and arguments — builds the skill faster than any explanation, because each trace is a concrete story of how a program reached a failure. Over time, you will learn to skim traces, recognising familiar library frames and jumping straight to your own code, and to spot patterns, like the repeated frames of infinite recursion or the single frame that appears in every occurrence of a recurring bug.

A final note on stack traces and anonymous functions, from Section 32: a trace containing anonymous frames is harder to read, because the frames lack names to identify them, and that is one more reason to name functions that appear in error-prone paths. A trace that names its functions tells a story with characters; a trace full of anonymous frames tells a story with blanks. Naming is therefore not just a readability habit but a debugging aid, and it pays off precisely when a program fails, which is when help is most needed. Read traces from the top, walk down to the origin, and let the chain of calls tell you where the wrongness began.

Check: When reading a stack trace, why should you look beyond the top frame?

Section 40 — Missing arguments and undefined parameters

When a function is called with fewer arguments than it declares parameters, the missing parameters hold undefined inside the function. That is not an error; it is JavaScript's forgiving behaviour, and it produces results that are technically defined but practically wrong, like a greeting that says "Hello, undefined!" or a calculation that yields NaN because one of its inputs was undefined. The symptom is a value that is undefined or NaN in a place where a real value was expected, and the diagnosis is to check the call site: how many arguments were supplied, and do they match the parameters in count and order? That check, applied before inspecting the function's body, resolves a large share of "function returns undefined" bugs, because the body is often correct and the call is short.

Default parameters, from Section 11, are the designed remedy for missing arguments. A parameter with a default receives the default when its argument is missing or explicitly undefined, so a greeting function with a default name never greets "undefined", and a calculation function with a default rate never multiplies by undefined. Defaults turn missing arguments from a silent wrong result into a defined behaviour, which is why they should be used for any parameter that has a sensible fallback. But defaults do not fix every missing-argument problem: a parameter without a sensible default — a required input, like the data to be processed — cannot be defaulted meaningfully, and calling the function without it is a caller error that should be caught by the caller, not papered over by a default that hides the omission.

There is also a validation approach for required parameters: at the start of a function, check whether a required parameter is undefined and, if so, handle it explicitly, either by throwing a clear error or by returning a sensible result. Throwing an error for a missing required argument is a loud failure that points at the caller immediately, which is preferable to a silent NaN that surfaces far downstream. That explicit check is a form of defensive programming, and it is most valuable for functions in shared code or libraries, where callers may be unfamiliar with the function's requirements and where a clear error saves them debugging time. For functions in your own small programs, the check may be unnecessary overhead, and a default or a careful call site may suffice. The judgement is about who calls the function and how much help they need when they get it wrong.

Finally, missing arguments interact with the arguments-count check in a subtle way: extra arguments are ignored, so a call with too many arguments does not error, and a call with too few gives undefined rather than an error, which means JavaScript will not catch argument-count mistakes for you. That forgiveness places the responsibility on the developer: count arguments at call sites, use defaults for optional parameters, validate required ones explicitly when the function is shared, and test functions with the argument counts they will actually receive, including the edge case of no arguments. A function whose behaviour with missing arguments is defined — by a default, by a validation, or by a documented requirement — is a function that will not surprise its callers with undefined.

Check: What value does a missing parameter hold inside a function?

Section 41 — Arrow functions and implicit return

An arrow function with a single-expression body returns that expression's value automatically, without a return keyword, and that implicit return is one of the arrow function's most used features. The form const double = (n) => n * 2 defines a function that returns twice its input, and the absence of return is not an omission but the syntax: a body that is an expression, rather than a block in braces, is evaluated and its value is returned. That conciseness is ideal for short transformations, particularly as callbacks, where the function is a single operation: a list transformation that doubles each item, a condition that tests a property, a mapping that extracts a field. In those contexts, implicit return keeps the code focused on the operation rather than on the machinery of returning.

const double = (n) => n * 2;
double(5); // 10

The trap, introduced in Section 9, is the braces. An arrow function whose body is written in braces is a block of statements, not an expression, and it does not return implicitly: (n) => { n * 2; } evaluates the multiplication, discards the result, and returns undefined, because no return statement specifies a value. That behaviour is consistent with ordinary functions, whose braced bodies also need explicit returns, but it surprises developers who add braces to a working single-expression arrow — for example, to add a logging line — and forget to add a return, breaking a function that previously worked. The remedy is a rule: when you convert an expression body to a braced body, add a return for the value the expression used to provide, or the function will silently change its behaviour from returning a value to returning undefined.

A related detail is returning an object literal from a single-expression body. The form () => { name: "Ada" } is read as a block containing a labelled statement, not as an object, and it returns undefined, or throws a syntax error depending on the label's form. Wrapping the object in brackets, () => ({ name: "Ada" }), forces the braces to be read as an object literal, and the function returns the object. That wrapping is a small piece of syntax that every arrow-function user learns, usually by encountering the bug once, and it is worth knowing in advance so that the bug is recognised rather than puzzling. The pattern — brackets around an object literal in an expression position — appears elsewhere in JavaScript too, wherever an object literal might be mistaken for a block, and recognising it as a disambiguation device makes it feel less arbitrary. That disambiguation also appears when returning an object from a function stored in a variable, so it is worth recognising wherever it shows up.

When an arrow function surprises you, check its braces first, before questioning anything else.

Check: Why does (n) => { n * 2; } return undefined instead of twice n?

Section 42 — Multiple returns

A function can contain several return statements, one for each case it handles, and that is often the clearest way to structure a function with distinct outcomes. A function that classifies a score might return "Excellent" for scores above ninety, "Good" for scores above seventy, and "Needs work" otherwise, with a return for each case, each placed as soon as its case is identified. That structure reads as decisions, each concluding with its answer, and it avoids nesting the later cases inside the else branches of the earlier ones. Multiple returns, used this way, are an extension of the early-return pattern from Section 13, applied not just to guard clauses at the top but to the function's main logic throughout.

The concern sometimes raised about multiple returns is that they make a function's exit points harder to track: a reader must check every return to know all the values a function can produce, whereas a function with a single return at the end has one place where its result is determined. That concern has merit for long functions, where returns scattered through many lines are easy to miss, but it is less compelling for short functions, where each return is visible at a glance and the cases are few. The balance is therefore about length and clarity: in a short function with distinct cases, multiple returns clarify, because each case ends with its answer and the reader need not carry the case forward. In a long function with many returns, the exits can become hard to track, and the function may need splitting, so that each resulting function has fewer cases and fewer returns.

A useful discipline for multiple returns is to make each return's case explicit in the condition that precedes it, so that a reader can see, for each return, exactly when it fires. A bare return inside a deeply nested condition, with no clear statement of its case, is hard to reason about, while a return guarded by a clear condition at the function's top level is easy to understand and to test: that condition, that result. Testing also benefits from multiple returns, because each case can be tested independently: call the function with inputs for each case, and check that each returns its expected value, without needing to trace through nested logic to reach a later case. That testability is a practical advantage that mirrors the readability advantage: cases that are separate in code are separate in testing.

Finally, multiple returns interact with the single-job principle from Section 34. A function with many returns may be handling many cases of one job, which is fine, or it may be handling several jobs, each with its own returns, which is a sign it should be split. The test is whether the returns all serve the same purpose — different answers to the same question — or whether they serve different purposes — answers to different questions that happen to share a function. Different answers to the same question belong together; answers to different questions belong apart. Applying that test to a multi-return function tells you whether its returns are a strength, clarifying its cases, or a symptom, hiding its multiplicity. Either way, the returns themselves are not the problem; the structure they reveal is.

Check: When are multiple return statements clearer than a single return at the end?

Section 43 — Methods: functions on objects

A method is a function that belongs to an object, stored as one of the object's properties and called through the object. The form user.greet() calls the greet method of the user object, with the dot connecting the object to its method, and inside the method, the keyword this refers to the object the method was called on, so that the method can access the object's other properties. Methods are how objects encapsulate behaviour with data: an object representing a user can have a method that returns its full name from its first and last name properties, an object representing a cart can have a method that calculates its total from its items, and an object representing a counter can have methods to increment, decrement, and read its value. That pairing — data plus the operations on that data, in one unit — is the core idea of object-oriented organisation, which later chapters explore in full.

const user = {
  firstName: "Ada",
  lastName: "Okafor",
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};
user.fullName(); // "Ada Okafor"

Methods are defined in object literals with a shorthand syntax, as in the example, where fullName() { } inside the object defines a method without needing the function keyword or a colon. That shorthand is the modern style, and it reads naturally: the object lists its properties and its methods together, each named, each doing its part. Inside a method, this is the mechanism that connects the method to its object, and it is why methods are usually written as ordinary functions rather than arrow functions, as Section 10 explained: an ordinary function's this is set by the call, so user.fullName() sets this to user, while an arrow function's this is inherited from its definition context, which would not be user. That distinction is the practical reason method definitions use the ordinary form, even in codebases that use arrows elsewhere.

Methods raise a question about scope that is worth noting: a method's body is a function body, so it has function scope, and its parameters and locals follow the same rules as any function's. Its special feature is this, which gives it access to the object's properties as if they were variables, through this.propertyName. That access is convenient, but it also creates a dependency: the method assumes it will be called on an object with the expected properties, and calling it on a different object, or detaching it from its object and calling it plainly, changes what this refers to and can produce errors or wrong values. That fragility is a known aspect of methods, and later chapters cover techniques for managing it, including binding a method to its object. For now, the rule is to call methods through their objects, as user.fullName(), and to expect this inside a method to be that object.

Check: What does this refer to inside a method called as user.fullName()?

Section 44 — Scope in loops

Loops create a scope question that is subtle and important: does a variable declared in a loop belong to the loop, or to each iteration separately? For let and const, the answer is per iteration, in the sense that each iteration gets a fresh binding of a variable declared in the loop's header or body, so that a closure created inside one iteration captures that iteration's value rather than a shared value. That per-iteration freshness is what makes creating callbacks inside a loop work correctly with let: each callback remembers its own iteration's value, because each iteration had its own variable. It is also what makes loop variables safe to use after the loop in the limited sense that they do not leak: a let declared in a loop is not visible after the loop, because its scope is the loop, and the loop has ended.

for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2
}
// i is not visible here

The contrast with var, from Section 20, is sharp. A var declared in a loop belongs to the enclosing function, not to the loop, and it is shared across all iterations, so that a closure created inside the loop captures the shared variable, whose value by the time the closure runs is the loop's final value, not the iteration's value. That shared-variable behaviour is the classic loop-closure bug, and it produced countless confusing results in old code, where a set of buttons created in a loop all reported the same index, or a set of timers all fired with the same value. The fix in old code was to create a fresh scope per iteration, usually with an IIFE, as Section 28 described. The fix in modern code is simply to use let, whose per-iteration binding does that work automatically. That automatic freshness is one of the strongest practical reasons to prefer let over var in loops.

There is a related point about variables declared inside a loop's body versus its header. A variable declared in the header, like the i in the example, is fresh per iteration by the language's design for let. A variable declared in the body is also fresh per iteration, because the body's block executes anew each time, creating new bindings for its let and const declarations. Either way, the iteration's variables are its own, and that ownership is what closures capture. Understanding that ownership — each iteration is a fresh scope for its let and const variables — turns the loop-closure problem from a mystery into a predictable consequence of scoping, and it tells you exactly when to expect independence (with let) and when to expect sharing (with var).

For everyday loop writing, the practical rules are simple: declare loop variables with let, declare per-iteration temporaries inside the loop body, and do not expect loop variables to be available after the loop. Those rules keep loops self-contained, make closures inside loops behave as expected, and prevent the leakage that var allowed. Loops are where scope becomes visible in action, because the same code runs many times with different values, and the question of whether those values are shared or fresh is the question that determines whether the loop's results are correct.

Check: Why does let in a loop give each iteration its own variable?

Section 45 — Closures in loops: the var pitfall

The var loop pitfall is the most famous closure bug in JavaScript, and it deserves a section of its own because it has confused generations of developers and because it illustrates, in a single example, how scope, closures, and var's function scoping interact. Consider a loop that creates three functions, each intended to report its own iteration number, using var for the loop variable. Because var is function-scoped, there is one shared variable for the whole loop, and each created function closes over that same variable. By the time any of the functions is called, after the loop has finished, the shared variable holds its final value — the value that ended the loop — so all three functions report the same number, the final one, rather than their own iteration numbers. The functions are correct in their code; they are wrong in their captured variable, because they all captured the same one.

var functions = [];
for (var i = 0; i < 3; i++) {
  functions.push(function () {
    return i;
  });
}
functions[0](); // 3, not 0

The explanation, once seen, is clear, but it was not clear to the many developers who met it as a bug, because the code looks as if each iteration should have its own i. The loop's text suggests three iterations with three values, and the functions' text suggests each returns its iteration's value, but var's scoping means there is one i, shared, and the closures all refer to it. The bug is therefore a scoping bug, not a closure bug in the abstract: closures work exactly as designed, remembering their defining scope, but the defining scope they remember is the function's scope, shared by all iterations, rather than a per-iteration scope, which var does not create. Understanding that distinction — closures remember scopes, and var's scope is the function — turns the pitfall from a mystery into a consequence.

The modern fix is let, whose per-iteration binding, from Section 44, gives each iteration its own variable, so each closure captures its own value, and the three functions report 0, 1, and 2 as intended. Simply changing var to let in the loop header fixes the example completely, which is a striking demonstration of how a scoping choice changes behaviour without changing any other line. The historical fix, for code that must use var, was to create a per-iteration scope manually, usually with an IIFE that took the iteration's value as a parameter, giving each closure its own parameter to capture. That fix worked, but it was verbose and obscure, and it is now a historical curiosity rather than a technique to use, because let solves the problem at its root.

The lesson generalises beyond loops. Whenever a closure captures a variable, ask whether that variable is shared with other closures or fresh to this one, and whether sharing is intended. Shared capture is correct when closures should see each other's updates, as in the counter's single count, and incorrect when each closure should have its own value, as in the loop's iterations. That question — shared or fresh? — is the closure design question, and answering it correctly requires knowing the captured variable's scope.

Check: Why do functions created in a var loop all report the loop's final value?

Section 46 — Module scope

Module scope is the scope created by a JavaScript module, a file that is loaded as a module rather than as a classic script. Variables declared at the top level of a module are scoped to that module, not to the global scope, so that two modules can each declare a variable with the same name without clashing, and so that a module's internal variables do not leak into the global scope or into other modules. Sharing between modules happens explicitly, through exports and imports: a module exports the values it wants to share, and another module imports them by name, making the sharing visible in code rather than implicit through globals. That explicitness is the module system's central virtue, and it is why modules have become the standard way to organise JavaScript programs beyond a single file.

Module scope solves the global pollution problem that IIFEs, from Section 28, were invented to address, but it solves it more cleanly, because modules are a language feature rather than a wrapping trick. A classic script's top-level let and const variables are global within that script's scope but do not become window properties, which is already an improvement over var, but they can still clash conceptually with other scripts' globals if the scripts share a scope through concatenation or through careless loading. A module's top-level variables are private to the module by default, which is a stronger guarantee: privacy is the default, and sharing is opt-in through exports. That default aligns with the smallest-sufficient-scope discipline from Section 19, extended to the file level: a value is visible only where it is needed, and needed-ness is declared by import.

The practical consequences for a developer learning modules are straightforward. Code in a module can use its own top-level variables freely, without checking other files for collisions. Code that needs a value from another module imports it, and the import statement at the top of the file lists the file's dependencies visibly, so that a reader can see what a module depends on before reading its body. Code that provides a value for others exports it, and the export statement marks the value as part of the module's public interface, distinct from its private internals. That public-private distinction, enforced by the module system rather than by convention, is a powerful organising tool, and it scales: a program of dozens of modules remains comprehensible, because each module's interface is explicit and its internals are hidden.

This book covers modules in full in a later chapter, including the syntax for exports and imports, the difference between default and named exports, and the loading behaviour of modules in browsers. This section's job is to place module scope in the scope hierarchy you now understand: global scope encloses everything, module scope encloses a file's code, function scope encloses a function's code, and block scope encloses a block's code, with visibility flowing inward along that nesting. A variable declared in a module is visible throughout that module and, if exported, to modules that import it, but not to modules that do not. That is scope, applied at the scale of files, and it completes the picture that began with the simple idea that a name is visible inside its box and not outside it.

Check: How does module scope differ from global scope?

Section 47 — Avoiding global pollution

Global pollution is the accumulation of variables and functions in the global scope, where they can clash with each other, with other scripts, and with future additions to the language or to libraries. A page that loads several scripts, each declaring globals with common names like data, config, or utils, risks collisions where one script overwrites another's value, producing bugs that depend on load order and that are difficult to trace, because the overwriting script and the overwritten value may be in unrelated files. Global pollution also risks collisions with browser-provided globals and with future language additions: a global named with a word that later becomes a built-in or a reserved word can break when the environment changes, even though the program itself did not change. Keeping the global scope clean is therefore a form of future-proofing as well as a form of tidiness.

The techniques for avoiding pollution follow directly from the scope mechanisms in this chapter. The first is to declare variables inside functions and blocks, in the smallest scope that needs them, rather than at the top level, so that most names never reach the global scope at all. The second is to use modules, whose top-level declarations are module-scoped rather than global, so that a file's internals are private by default. The third, for classic scripts that cannot use modules, is to wrap code in a function or an IIFE, so that its variables live inside that function's scope rather than globally, exposing only a single global — if any is needed — as the script's public interface. That single-global pattern, where a library exposes one object holding all of its functionality, was the standard approach before modules, and it remains a valid fallback: one global name, carefully chosen to be distinctive, is far safer than dozens of common names.

There is also a discipline of naming for the globals that must exist. A global that is genuinely needed — a configuration object, a library's entry point — should have a distinctive name unlikely to collide, often prefixed with the project or library name, rather than a generic name like data or helper. That distinctiveness is a social convention, not a language guarantee, but it works in practice because collisions require two parties to choose the same distinctive name, which is unlikely when names are project-specific. Combined with minimising the number of globals, distinctive naming reduces collision risk to a level that is manageable even on pages with many scripts.

Finally, global pollution is worth checking for, not just avoiding in principle. A developer can inspect the global scope in the Console, by examining the window object or by listing its properties, and can see whether a page's scripts have left unexpected globals behind. That inspection, done occasionally during development, reveals pollution that was introduced accidentally — a forgotten var at the top level, a function declaration outside any module or wrapper — while it is still easy to fix, by moving the declaration into its proper scope. A clean global scope is not an aesthetic preference; it is a measurable property of a page, and keeping it clean is part of shipping code that plays well with others.

Check: What is global pollution, and why should you avoid it?

Section 48 — Refactoring repeated code into a function

Refactoring repeated code into a function is the practical act behind the principle of not repeating yourself, and it follows a simple rhythm: notice repetition, identify the varying parts, extract the common part into a function with those varying parts as parameters, and replace each repetition with a call. Consider three places in a program that each build a greeting message, differing only in the name greeted. The common part is the greeting construction; the varying part is the name. Extracting a function greet(name) that builds the greeting, and replacing each repetition with a call passing the appropriate name, removes two copies of the logic and leaves one, which is easier to maintain, easier to test, and easier to read, because each call site now says "greet this name" rather than spelling out how greeting works.

The identification of varying parts is the skill in this refactoring. Repetition is rarely exact: the copies differ in a value, a condition, or a detail, and those differences become the function's parameters, while the parts that are identical become the function's body. When the differences are many or complex, the extraction may need several parameters, or it may reveal that the copies are not truly the same job and should not be merged, which is itself useful information. A good extraction has a clear name for the job and a short parameter list for the variations; a bad extraction has a vague name and a long parameter list full of flags that switch between behaviours, which is a sign that the copies were different jobs forced into one function. The "and" test from Section 34 applies here too: if the extracted function's job description needs "and" to join unrelated tasks, it should be two functions, not one.

There is a practical order to refactoring that reduces risk. First, make sure the repeated code is correct and tested in its current form, so that you have a baseline to compare against. Then extract the function, replacing one repetition at a time and testing after each replacement, so that a mistake in extraction is caught immediately rather than after all repetitions have been changed. Then run the program's full checks, confirming that behaviour is unchanged, because refactoring by definition changes structure without changing behaviour. That sequence — baseline, extract incrementally, verify — is the safe form of refactoring, and it applies to larger refactorings as well as to this small one. Refactoring without verification is merely editing, and editing without verification is how working programs break.

Refactoring is also a habit to practise deliberately, not just a technique to apply when repetition becomes painful. When you catch yourself writing the same code a second time, pause and consider extracting it before writing it a third time. The second copy is the signal; the third copy is the debt. That discipline, applied consistently, keeps programs from accumulating the duplicated logic that makes maintenance expensive, and it builds the function-design judgement that this chapter teaches: what is a job, what are its inputs, what does it return, and what should it be called? Each extraction is a small exercise in those questions, and the accumulated exercises are what turn a developer who writes functions into a developer who designs with them.

Check: What are the steps for turning repeated code into a function?

Section 49 — Testing functions by thinking

Before running code, you can test a function by thinking: tracing its execution mentally with specific inputs, predicting its output, and comparing that prediction with what the function should do. That mental testing, sometimes called desk-checking or tracing, is a skill worth practising, because it catches bugs before they reach the Console and because it deepens understanding of the function in a way that running it does not. The technique is simple: choose an input, walk through the function's body line by line, tracking each variable's value as it changes, following each condition to its branch, and noting each return, until you reach the function's result. Then ask whether that result is correct for the chosen input. If it is not, you have found a bug without running anything. If it is, choose another input, particularly an edge case — an empty value, a zero, a maximum, a missing argument — and trace again.

Mental testing is most valuable for the cases that automated testing might miss, because the tester chooses inputs deliberately to probe the function's logic: the boundary values where conditions change, the combinations where two conditions interact, the inputs that exercise each return path. A function with three branches should be traced with at least one input per branch, so that every return is exercised mentally. A function with a loop should be traced with zero iterations, one iteration, and several iterations, so that the loop's start, middle, and end are all checked. A function with a default parameter should be traced with the argument supplied and with it missing, so that the default's behaviour is verified. That systematic choice of inputs — boundaries, branches, and edge cases — is the core of good testing, whether mental or automated, and it is a habit that transfers directly to writing formal tests when you reach the testing chapter.

It is slow for long functions, it is error-prone for complex calculations, and it cannot catch bugs that depend on the environment — the page's state, the network, the timing of events — because those are not visible in the function's text alone. Mental testing is therefore a complement to running code, not a replacement: trace a function mentally to understand it and to catch logic errors, then run it with real inputs to confirm, and then, when the testing chapter arrives, write automated tests that capture the traces as repeatable checks. That progression — think, run, automate — is the natural path from understanding a function to guaranteeing it, and each step builds on the previous one.

A final benefit of mental testing is that it teaches reading. A developer who can trace a function mentally can read unfamiliar code effectively, because tracing is reading with attention to values and flow rather than skimming for gist. That reading skill is the foundation of debugging, of code review, and of maintaining other people's programs, all of which are daily work for a professional developer. Practise tracing on the functions in this chapter: pick a function, pick an input, walk through it on paper or in your head, and predict the result before checking it by running. The accuracy of your predictions will improve with practice, and with it, your confidence in reading and writing functions.

Check: Why is it useful to trace a function mentally before running it?

Section 50 — Chapter 17 in one paragraph

Functions are named, reusable blocks of code that take values in through parameters, work with them in a body, and hand results back through return values, and they come in several forms — declarations, expressions, and arrow functions — each suited to different situations, with defaults and rest parameters making their inputs flexible and callbacks and higher-order functions making behaviour itself passable as a value. Around functions sits scope, the system of nested boxes that decides where each name is visible: global scope for the whole program, function scope for a function's locals, block scope for let and const inside braces, and module scope for a file's private names, with the scope chain resolving each use by searching outward and with shadowing, hoisting, and the temporal dead zone as the consequences of that nesting that every developer must recognise. Closures extend that system by letting functions remember their defining scope, which gives private, persistent state without globals, and the disciplines that tie it all together — one job per function, parameters instead of globals, clear names, early returns, and testing by tracing — are what turn functions from a syntax feature into a design practice. Write functions that do one job well, give them honest parameter lists and clear names, keep their variables in the smallest scope that needs them, and your programs will be easier to understand, to test, and to fix, which is the whole point of organising code into functions in the first place. Look back over the chapter and notice the two threads running through it, and how they intertwine. The first thread is functions as units of work: define a job once, give it a name and parameters, call it with arguments, and receive a result through return, with variations — expressions, arrows, defaults, rest parameters, callbacks, and higher-order functions — that make the basic shape flexible enough for real programs. The second thread is scope as the system that governs names: boxes nested inside boxes, with visibility flowing inward, with the scope chain resolving each use by searching outward, and with closures as the consequence that lets a function carry its defining box with it after that box would otherwise have closed. The threads meet everywhere: parameters are function-scoped variables, closures are functions plus scope, the loop pitfall is a scoping rule meeting a closure, and the disciplines of the chapter — one job per function, parameters instead of globals, smallest sufficient scope, clear names — are all ways of keeping the two threads tidy so that programs stay understandable as they grow. Hold both threads together, and the next chapters, on data structures, events, and asynchronous code, will build on ground you already own.

That summary is worth memorising, because every later chapter in this book assumes it without re-explaining it.

Check: In your own words, summarise the relationship between functions and scope.

Practical task

Build a small page with a script that: defines a function that takes a name and an age and returns a greeting sentence (using a default parameter for the name); defines a second function that uses a closure to create a counter, and uses it to count how many greetings have been made; and defines a third function that takes any number of scores with a rest parameter and returns their average. Call each function from your page's script, print the results to the Console, and then deliberately test the edge cases: call the greeting function with no arguments, call the counter twice and confirm it keeps counting, and call the average function with no scores to see what happens. Write down, in a sentence each, what each edge case taught you, then fix the average function so that it handles no scores gracefully instead of returning a confusing result.

Chapter questions

  1. Explain the difference between a parameter and an argument, using an example of your own.
  2. A function returns undefined when you expected a number. List three possible causes, in the order you would check them.
  3. Explain what a closure is, and describe a practical situation where a closure is the right tool.
  4. Why does changing var to let fix the loop-closure problem, and what does that tell you about scope?
  5. Write a function that takes any number of prices with a rest parameter, returns their total, and uses a default parameter for a discount rate. Then explain, in your own words, what each parameter does.