HTML, CSS, JavaScript Basics

Chapter 9
Functions, Scope, Errors and Modules

Bachelors Program in Full Stack Development
HTML, CSS, JavaScript Basics   •   Bachelor

Functions, Scope, Errors and Modules

Introduction

As JavaScript programs grow beyond a few lines, placing all instructions in one sequence becomes difficult to understand, test, and maintain. Functions provide reusable units of behavior; scope controls where names can be accessed; closures allow functions to retain access to surrounding data; error handling helps programs respond safely to failure; and modules divide an application into coherent files with explicit dependencies.

These concepts are central to professional front-end and full-stack development. A web application may contain functions for validating forms, fetching data, transforming responses, updating the interface, and handling user actions. Without clear boundaries between these responsibilities, changes become risky and defects become difficult to locate. This chapter develops the conceptual and practical foundations needed to design reliable JavaScript code.

Learning Objectives

By the end of this chapter, you should be able to:

  • Define and invoke function declarations, expressions, and arrow functions.
  • Use parameters, default values, rest parameters, and return values appropriately.
  • Distinguish local, block, function, and global scope.
  • Explain lexical scope, closures, and common closure-related design patterns.
  • Apply basic functional programming ideas such as pure functions, immutability, and higher-order functions.
  • Detect, throw, and handle JavaScript errors using `try`, `catch`, `finally`, and `Error` objects.
  • Explain asynchronous error handling with promises and `async`/`await`.
  • Organize code using ES modules and understand named and default exports.
  • Evaluate maintainability, security, and testing implications of function and module design.

1. Functions as Reusable Abstractions

A function is a named or otherwise referenceable block of code designed to perform a task. It can receive input, perform operations, and optionally produce an output. Functions support abstraction: callers need to know what a function does and how to use it, but normally do not need to know every internal instruction.

A useful function generally has:

1. A clear purpose.

2. Explicit inputs represented by parameters.

3. A predictable output or observable effect.

4. Limited dependence on unrelated global state.

5. A name that communicates its behavior.

Function declarations

A function declaration uses the `function` keyword:

```js

function calculateTotal(price, quantity) {

return price * quantity;

}

const total = calculateTotal(15, 3); // 45

```

`price` and `quantity` are parameters. The values `15` and `3` are arguments. The `return` statement ends the function and supplies its result to the caller.

Function declarations are hoisted: JavaScript makes the declaration available before its position in the surrounding scope is reached.

```js

showMessage();

function showMessage() {

console.log("Welcome");

}

```

Although this behavior is valid, placing declarations before their use usually improves readability.

Function expressions

A function expression creates a function and assigns it to a variable:

```js

const calculateArea = function (width, height) {

return width * height;

};

```

The function is available only after the assignment has been evaluated. Function expressions are useful when functions are passed as values, stored in data structures, or selected conditionally.

```js

const operation = function (value) {

return value * 2;

};

```

Functions are first-class values in JavaScript. They can be assigned to variables, passed as arguments, returned from other functions, and stored in arrays or objects.

Arrow functions

Arrow functions provide concise syntax:

```js

const square = (number) => number * number;

const add = (a, b) => a + b;

```

For a single parameter, parentheses may be omitted, although retaining them can improve consistency:

```js

const square = number => number * number;

```

A block-bodied arrow function requires an explicit `return`:

```js

const square = number => {

return number * number;

};

```

Arrow functions differ from traditional functions in their treatment of `this`, `arguments`, and constructor behavior. Most importantly, an arrow function does not create its own `this`; it captures `this` from its surrounding lexical context. This makes arrows useful for callbacks, but ordinary methods or constructor functions may require regular function syntax.

2. Parameters, Arguments, and Return Values

Parameters define the expected inputs of a function, but JavaScript does not require callers to provide exactly the declared number of arguments. Missing arguments receive the value `undefined`, and additional arguments can be accessed through rest parameters.

Default parameters

Default parameters provide a value when an argument is `undefined`:

```js

function greet(name = "Guest") {

return `Hello, ${name}`;

}

greet(); // "Hello, Guest"

greet("Amina"); // "Hello, Amina"

```

A default is used for an omitted argument or an explicitly supplied `undefined`, but not normally for `null`:

```js

greet(null); // "Hello, null"

```

If an input must be present, validate it explicitly rather than relying on a default.

Rest parameters

A rest parameter collects remaining arguments into an array:

```js

function sum(...numbers) {

return numbers.reduce((total, number) => total + number, 0);

}

sum(2, 4, 6); // 12

```

Only one rest parameter may appear, and it must be last. It differs from the older `arguments` object because it is a genuine array and works naturally with array methods.

Returning values

A function with no explicit `return` produces `undefined`. `return` also stops execution immediately:

```js

function divide(a, b) {

if (b === 0) {

return null;

}

return a / b;

}

```

Returning `null` can communicate an intentional absence of a result, whereas `undefined` often indicates that no value was supplied. The appropriate convention should be documented and used consistently.

A function should generally return one coherent conceptual result. If several related values are required, an object can make the result self-describing:

```js

function parseName(fullName) {

const parts = fullName.trim().split(" ");

return {

firstName: parts[0] ?? "",

lastName: parts.slice(1).join(" ")

};

}

```

3. Scope and Lexical Environments

Scope determines where an identifier can be accessed. JavaScript uses lexical scope: access is determined by where code is written, not by where a function is called.

Block scope

Variables declared with `let` and `const` are block-scoped. A block is commonly delimited by braces in an `if` statement, loop, or standalone block.

```js

if (true) {

const message = "Inside";

console.log(message);

}

// message is not accessible here

```

`var` is function-scoped rather than block-scoped and should generally be avoided in modern JavaScript because it can create confusing behavior:

```js

if (true) {

var oldStyle = "visible outside the block";

}

console.log(oldStyle);

```

Function scope and global scope

A parameter and variables declared inside a function are local to that function:

```js

function createUser() {

const role = "student";

return role;

}

```

`role` cannot be referenced outside `createUser`. A variable declared at the top level of a browser script may become global, depending on the script type and declaration mechanism. Excessive global state creates hidden dependencies and name collisions. Prefer modules and local variables.

Scope chain and shadowing

When JavaScript evaluates an identifier, it searches the current scope, then enclosing lexical scopes, and finally outer scopes. This is the scope chain.

```js

const theme = "light";

function display() {

const message = "Page";

console.log(theme, message);

}

```

The function can read `theme` because the outer scope is available to it. An inner declaration may shadow an outer one:

```js

const status = "global";

function report() {

const status = "local";

return status;

}

```

The result is `"local"`. Shadowing is legal but excessive use can reduce clarity.

Temporal Dead Zone

Variables declared with `let` and `const` are not usable before their declaration is evaluated:

```js

console.log(value); // ReferenceError

const value = 10;

```

The interval from entering the scope until declaration initialization is called the temporal dead zone. This behavior prevents accidental use of uninitialized variables. Declarations should normally occur before use.

4. Closures

A closure is a function together with the lexical environment in which it was created. Consequently, an inner function may continue to access variables from an outer function after the outer function has returned.

```js

function createCounter() {

let count = 0;

return function () {

count += 1;

return count;

};

}

const counter = createCounter();

counter(); // 1

counter(); // 2

```

The variable `count` is not directly accessible from outside, but the returned function retains access to it. This is a form of encapsulation.

Closures are useful for:

  • Maintaining private state.
  • Creating configurable functions.
  • Implementing callbacks and event handlers.
  • Building function factories.
  • Supporting memoization and controlled caching.

A function factory demonstrates configurable behavior:

```js

function createMultiplier(factor) {

return number => number * factor;

}

const double = createMultiplier(2);

const triple = createMultiplier(3);

```

Each returned function closes over a different `factor`.

Closures can also cause accidental memory retention. If a long-lived event listener or timer refers to a large object through a closure, that object may remain reachable longer than intended. Remove listeners and cancel timers when components or pages are no longer needed.

5. Functional Programming Concepts

JavaScript supports multiple programming styles. Functional programming emphasizes composing operations with functions and minimizing unpredictable state changes.

Pure functions

A pure function has two important properties:

  • The same inputs always produce the same output.
  • It has no observable side effects, such as changing external variables, modifying the DOM, or performing network requests.

```js

function formatPrice(amount) {

return `$${amount.toFixed(2)}`;

}

```

By contrast, this function changes external state:

```js

let total = 0;

function addToTotal(amount) {

total += amount;

}

```

Pure functions are easier to test and reason about. Side effects are not inherently wrong; applications must eventually update interfaces, store data, or communicate over networks. A strong design isolates effects at clear boundaries and keeps data transformation functions pure where practical.

Higher-order functions

A higher-order function accepts another function, returns a function, or both. Array methods illustrate this approach:

```js

const prices = [10, 20, 30];

const withTax = prices.map(price => price * 1.2);

const expensive = prices.filter(price => price >= 20);

const total = prices.reduce((sum, price) => sum + price, 0);

```

`map` transforms each element and returns a new array. `filter` retains elements satisfying a condition. `reduce` combines elements into an accumulated result. These methods express intent more directly than many manual loops, although a loop may be preferable when operations are complex or performance requirements demand it.

Immutability and copying

Immutability means avoiding direct modification of existing data. For objects and arrays, spread syntax creates shallow copies:

```js

const user = { name: "Lee", active: true };

const updatedUser = { ...user, active: false };

```

The original object remains unchanged. A shallow copy does not recursively copy nested objects, so nested data requires additional copying or an appropriate data-management strategy.

6. Errors and Defensive Programming

An error is a condition that prevents an operation from completing normally or makes its result invalid. Errors may result from invalid user input, unavailable servers, malformed data, programming defects, or environmental conditions.

JavaScript includes built-in error types such as `Error`, `TypeError`, `ReferenceError`, `RangeError`, and `SyntaxError`. The type often provides useful diagnostic information, but applications should not expose internal details to end users.

Throwing errors

Use `throw` when a function cannot fulfill its contract:

```js

function withdraw(balance, amount) {

if (!Number.isFinite(amount) || amount < 0) {

throw new TypeError("Amount must be a non-negative number");

}

if (amount > balance) {

throw new Error("Insufficient funds");

}

return balance - amount;

}

```

Throwing an `Error` object preserves a message and stack trace. Do not normally throw strings or arbitrary values because they provide less consistent diagnostic information.

Handling errors

`try` encloses code that may fail. `catch` handles a thrown error, and `finally` runs whether an error occurs or not:

```js

try {

const result = withdraw(100, 150);

console.log(result);

} catch (error) {

console.error("Transaction failed:", error.message);

} finally {

console.log("Transaction attempt completed");

}

```

A `catch` block should respond meaningfully: show a safe message, retry where appropriate, record diagnostic information, or pass the error to a higher-level handler. Empty catch blocks conceal defects and make production diagnosis difficult.

Modern JavaScript permits optional catch binding when the error object is unnecessary:

```js

try {

JSON.parse("invalid");

} catch {

console.log("Input was not valid JSON");

}

```

Error propagation and custom errors

If a function cannot resolve an error, it can allow the error to propagate to a caller that has sufficient context. Custom error classes distinguish categories:

```js

class ValidationError extends Error {

constructor(message, field) {

super(message);

this.name = "ValidationError";

this.field = field;

}

}

```

Callers can inspect the error using `instanceof`, but error handling should not depend solely on message text, which may change.

Asynchronous errors

A `try`/`catch` around the creation of a promise does not automatically catch a later rejection. With `async`/`await`, place the awaited operation inside the `try` block:

```js

async function loadUser(id) {

try {

const response = await fetch(`/api/users/${id}`);

if (!response.ok) {

throw new Error(`Request failed: ${response.status}`);

}

return await response.json();

} catch (error) {

console.error("Unable to load user", error);

throw error;

}

}

```

The Fetch API commonly resolves its promise even for HTTP error statuses; therefore, checking `response.ok` is necessary when an unsuccessful status should be treated as an error. Network failures and parsing failures must also be considered.

7. ES Modules

A module is a file whose variables are private to that file unless explicitly exported. Modules provide encapsulation and make dependencies visible. In browser HTML, a module is loaded with:

```html

<script type="module" src="/js/main.js"></script>

```

Module scripts are deferred by default, execute in strict mode, and are loaded once per module graph. They use URL-like paths for relative imports, including the file extension in many browser environments.

Named exports and imports

```js

// math.js

export function add(a, b) {

return a + b;

}

export const taxRate = 0.2;

```

```js

// checkout.js

import { add, taxRate } from "./math.js";

const subtotal = add(10, 5);

const tax = subtotal * taxRate;

```

Names must correspond to exported bindings, although imports may be renamed:

```js

import { add as sum } from "./math.js";

```

Default exports

A module may have one default export:

```js

// logger.js

export default function log(message) {

console.log(message);

}

```

It can be imported with any local name:

```js

import writeLog from "./logger.js";

```

Named exports are often preferable for libraries because they make available functionality explicit. Default exports can be convenient when a module has one primary responsibility. A project should adopt a consistent convention.

Re-exporting and module design

A module can re-export functionality:

```js

export { add, taxRate } from "./math.js";

```

A good module usually has high cohesion: its contents belong together. It should expose a small public interface and hide implementation details. Avoid circular dependencies, where module A depends on B while B depends directly or indirectly on A; these can produce partially initialized bindings and confusing runtime behavior.

8. Worked Example: A Validated Task Module

The following example separates validation, data transformation, and user-interface behavior.

```js

// taskUtils.js

export function validateTaskTitle(title) {

if (typeof title !== "string" || title.trim() === "") {

throw new TypeError("A task title is required");

}

return title.trim();

}

export function createTask(title) {

return {

id: crypto.randomUUID(),

title: validateTaskTitle(title),

completed: false

};

}

```

```js

// taskStore.js

let tasks = [];

export function addTask(title) {

const task = createTask(title);

tasks = [...tasks, task];

return task;

}

export function getTasks() {

return [...tasks];

}

```

In this illustrative design, the store module keeps its array private. `getTasks` returns a new array, preventing callers from directly replacing the module’s internal array. However, the objects inside remain shared; a production implementation may also need immutable object updates or a more formal state-management approach.

```js

// main.js

import { addTask, getTasks } from "./taskStore.js";

const form = document.querySelector("#task-form");

const input = document.querySelector("#task-title");

const list = document.querySelector("#task-list");

function render() {

list.replaceChildren();

for (const task of getTasks()) {

const item = document.createElement("li");

item.textContent = task.title;

list.append(item);

}

}

form.addEventListener("submit", event => {

event.preventDefault();

try {

addTask(input.value);

input.value = "";

render();

} catch (error) {

console.error(error);

alert(error.message);

}

});

```

This example demonstrates several principles: validation occurs at a boundary, state is encapsulated, rendering is separated from storage, errors are handled near the user interaction, and modules make dependencies explicit. In production, `alert` would usually be replaced by an accessible status message, and user-generated text should be inserted with `textContent`, not unsafe HTML construction.

9. Applied Case: Refactoring a Student Registration Page

Background

Consider an illustrative university registration page. Its initial JavaScript file contains 500 lines: it validates fields, calculates fees, sends requests, displays messages, and stores temporary state in global variables.

Problem

The page exhibits three defects. A fee calculation is duplicated in two event handlers and produces inconsistent results. A failed network request leaves the submit button disabled. Finally, a helper function modifies a global `currentStudent` object, making tests dependent on execution order.

Analysis

The duplicated calculation violates the principle of a single source of truth. The disabled button is a cleanup concern that belongs in a `finally` block. The global mutation indicates excessive coupling and makes the function impure and difficult to reuse.

Approach and solution

The team extracts a pure function:

```js

export function calculateFee(baseFee, credits, rate) {

if (![baseFee, credits, rate].every(Number.isFinite)) {

throw new TypeError("Fee inputs must be finite numbers");

}

return baseFee + credits * rate;

}

```

Validation functions are placed in `validation.js`, API operations in `registrationApi.js`, and DOM event handling in `registrationPage.js`. The submission handler uses:

```js

async function submitRegistration(data) {

submitButton.disabled = true;

try {

const response = await registerStudent(data);

showSuccess(response.message);

} catch (error) {

showError("Registration could not be completed. Please try again.");

console.error(error);

} finally {

submitButton.disabled = false;

}

}

```

The interface receives a safe user-facing message, while technical details remain in diagnostic logs. Tests can call `calculateFee` with known inputs without creating a page or network connection.

Outcome

The refactored design reduces duplication, makes failure behavior predictable, and allows independent testing of validation, calculations, API communication, and rendering. It also makes later changes—such as adding a scholarship discount—more localized.

Learning points

  • Separate pure business rules from effects such as DOM updates and network calls.
  • Keep mutable state private where possible.
  • Use `finally` for cleanup that must occur on success and failure.
  • Expose small module interfaces rather than entire internal data structures.
  • Treat error messages shown to users and diagnostic information for developers as different outputs.

10. Professional Practice, Security, and Limitations

Good function design improves maintainability but does not eliminate all complexity. Very small functions can become fragmented if abstraction is applied mechanically. A function should represent a meaningful responsibility rather than merely wrapping one trivial expression.

Input validation must occur at trust boundaries, including form inputs, URL parameters, local storage, and server responses. Client-side validation improves user experience but cannot replace server-side validation because users can bypass browser code. Never insert untrusted input with `innerHTML` unless it has been carefully sanitized; prefer `textContent` for plain text.

Error handling also requires governance. Do not display stack traces, database details, tokens, or personal information to users. Logs should be access-controlled and should avoid unnecessary sensitive data. In regulated applications, logging and retention practices may be subject to privacy and security requirements.

Modules improve organization, but bundlers, package managers, and deployment systems introduce their own concerns. Dependencies should be reviewed, updated, and constrained according to project policy. Modern tooling may perform tree shaking, code splitting, and static analysis, but developers remain responsible for sound boundaries and secure dependency use.

Artificial intelligence tools can assist with generating tests, suggesting refactorings, and explaining errors. Generated code must be reviewed for incorrect assumptions, insecure input handling, licensing concerns, and inappropriate exposure of confidential data. AI assistance does not replace execution, testing, or professional accountability.

Best Practices Checklist

  • Give each function one clear responsibility.
  • Prefer descriptive names and explicit return contracts.
  • Validate inputs at system boundaries.
  • Prefer `const`; use `let` only when reassignment is required.
  • Avoid unnecessary global variables and hidden mutation.
  • Keep pure calculations separate from DOM, storage, and network effects.
  • Use closures deliberately and release long-lived listeners or timers.
  • Throw `Error` objects with useful types and messages.
  • Handle promise rejections and check HTTP response status where necessary.
  • Use `finally` for cleanup.
  • Keep module interfaces small and dependencies explicit.
  • Test success paths, invalid inputs, and failure paths.
  • Never expose sensitive implementation details in user-facing errors.

Chapter Summary

Functions are first-class JavaScript values that package reusable behavior. Parameters describe inputs, return values communicate results, and default and rest parameters support flexible interfaces. Lexical scope determines identifier access through nested environments, while `let` and `const` provide block scope and protect against many legacy `var` problems. Closures preserve access to outer variables and support encapsulation, factories, callbacks, and private state.

Functional techniques such as pure functions, higher-order functions, array transformations, and controlled immutability improve testability and clarity when applied appropriately. Errors should be detected, represented with meaningful `Error` objects, propagated to suitable callers, and handled without exposing sensitive details. Asynchronous operations require deliberate rejection handling, including HTTP status checks when using Fetch.

ES modules provide file-level encapsulation, explicit imports and exports, and a foundation for scalable application architecture. The most maintainable designs separate pure business logic from effects, protect internal state, validate untrusted input, and organize related behavior into cohesive modules.

Key Terms

Function: A reusable unit of JavaScript behavior.

Parameter: A named input declared by a function.

Argument: A value supplied when a function is called.

Return value: The result sent from a function to its caller.

Arrow function: A concise function syntax with lexical `this` behavior.

Scope: The region in which an identifier can be accessed.

Lexical scope: Scope determined by the written nesting of code.

Scope chain: The sequence of nested environments searched for an identifier.

Shadowing: Declaring an inner identifier with the same name as an outer identifier.

Closure: A function together with access to its surrounding lexical environment.

Pure function: A function with deterministic output and no observable side effects.

Higher-order function: A function that accepts or returns another function.

Immutability: The practice of avoiding direct modification of existing data.

Exception: An abnormal condition represented by a thrown value.

Error object: An object containing error information such as a name, message, and stack.

Promise rejection: The failure state of a promise-based asynchronous operation.

Module: A file with its own scope and explicit import/export interface.

Named export: An exported binding imported by its declared name or an alias.

Default export: A module’s primary export, limited to one per module.

Side effect: An observable operation beyond returning a value, such as changing the DOM.

Review Questions

Short Answer Questions

1. What is the difference between a parameter and an argument?

2. Why are `let` and `const` generally preferred to `var`?

3. What is a closure, and why is it useful?

4. What is the purpose of a `finally` block?

5. How do named exports differ from default exports?

Long Answer Questions

1. Explain JavaScript lexical scope, the scope chain, shadowing, and the temporal dead zone, using examples.

2. Compare function declarations, function expressions, and arrow functions. Include relevant differences in hoisting and `this` behavior.

3. Discuss pure functions, higher-order functions, and immutability, and evaluate their benefits and limitations in web applications.

4. Explain synchronous and asynchronous error handling in JavaScript, including error propagation and HTTP response checking with Fetch.

5. Describe how ES modules improve maintainability and identify risks associated with excessive coupling and circular dependencies.

Analytical / Application Questions

1. A form submission function validates input, updates the DOM, sends a request, and writes to global state. Propose a modular redesign and justify the boundaries between modules.

2. A counter factory returns a function that increments a private number. Explain the closure involved and identify one memory-management concern if the counter is attached to a long-lived event listener.

3. A network request fails, but the submit button remains disabled. Analyze the likely design error and write a correction using `try`, `catch`, and `finally`.

Multiple Choice Questions with Answers

Question 1

What does a function return when it reaches the end without an explicit return statement?

A. null

B. false

C. undefined

D. 0

Correct Answer: C. undefined

Question 2

Which declaration is block-scoped?

A. var

B. let

C. function in all contexts

D. A property on window

Correct Answer: B. let

Question 3

What mechanism allows an inner function to retain access to variables from an outer function after the outer function returns?

A. Hoisting

B. Closure

C. Destructuring

D. Coercion

Correct Answer: B. Closure

Question 4

Which array method creates a new array by transforming every element?

A. filter

B. reduce

C. map

D. find

Correct Answer: C. map

Question 5

Which statement correctly describes a pure function?

A. It must modify a global variable

B. It always performs network I/O

C. It gives consistent output for the same inputs and has no observable side effects

D. It can never accept parameters

Correct Answer: C. It gives consistent output for the same inputs and has no observable side effects

Question 6

Which block runs whether an exception is thrown or not?

A. if

B. catch

C. finally

D. switch

Correct Answer: C. finally

Question 7

What should normally be thrown for consistent JavaScript error handling?

A. An arbitrary string

B. An Error object

C. An HTML element

D. An array only

Correct Answer: B. An Error object

Question 8

Which check is commonly needed after using fetch when HTTP failure statuses should be treated as errors?

A. response.ok

B. response.length

C. response.errorType

D. response.failedRequest

Correct Answer: A. response.ok

Question 9

Which HTML declaration loads a file as an ES module?

A. <script module src="main.js">

B. <module src="main.js">

C. <script type="module" src="main.js">

D. <script es6 src="main.js">

Correct Answer: C. <script type="module" src="main.js">

Question 10

What is a key characteristic of a default export?

A. A module may have unlimited default exports

B. It must be imported using exactly the exported local name

C. A module can have one default export, imported with a chosen local name

D. It can only export primitive values

Correct Answer: C. A module can have one default export, imported with a chosen local name

HTML, CSS, JavaScript Basics
Chapter 9 of 15