Episode 1 — Fundamentals / 1.20 — Functions

1.20.e — Writing Reusable Logic

In one sentence: Great functions follow the DRY principle and single responsibility, strive to be pure (same input = same output, no side effects), compose well with other functions through higher-order patterns, and communicate intent through verb-based names and JSDoc documentation.

Navigation: ← 1.20.d — Arrow Functions · 1.20.f — Practice Problems →


1. DRY principle — Don't Repeat Yourself

When you notice the same logic appearing in multiple places, extract it into a function:

Before DRY (repetitive)

const price1 = 100;
const tax1 = price1 * 0.08;
const total1 = price1 + tax1;
console.log(`Item 1: $${total1.toFixed(2)}`);

const price2 = 250;
const tax2 = price2 * 0.08;
const total2 = price2 + tax2;
console.log(`Item 2: $${total2.toFixed(2)}`);

const price3 = 50;
const tax3 = price3 * 0.08;
const total3 = price3 + tax3;
console.log(`Item 3: $${total3.toFixed(2)}`);

After DRY (reusable function)

function calculateTotal(price, taxRate = 0.08) {
  const tax = price * taxRate;
  return price + tax;
}

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

console.log(`Item 1: ${formatPrice(calculateTotal(100))}`);  // $108.00
console.log(`Item 2: ${formatPrice(calculateTotal(250))}`);  // $270.00
console.log(`Item 3: ${formatPrice(calculateTotal(50))}`);   // $54.00

Benefits of DRY:

  • One place to fix bugs — fix calculateTotal once, not in three places.
  • Consistent behavior — every call uses the same logic.
  • Easier testing — test the function in isolation.
  • Readable — the function name explains what happens.

2. Single Responsibility Principle (SRP)

Each function should do one thing and do it well.

Bad — does too much

function processUserRegistration(name, email, password) {
  // Validate
  if (!name || name.length < 2) throw new Error("Invalid name");
  if (!email.includes("@")) throw new Error("Invalid email");
  if (password.length < 8) throw new Error("Password too short");

  // Hash password
  const hashed = password.split("").reverse().join(""); // simplified

  // Save to database
  const user = { name, email, password: hashed, createdAt: Date.now() };
  database.push(user);

  // Send welcome email
  console.log(`Sending welcome email to ${email}`);

  // Log analytics
  console.log(`Analytics: new user registered — ${name}`);

  return user;
}

Good — each function has one job

function validateRegistration(name, email, password) {
  if (!name || name.length < 2) throw new Error("Invalid name");
  if (!email.includes("@")) throw new Error("Invalid email");
  if (password.length < 8) throw new Error("Password too short");
}

function hashPassword(password) {
  return password.split("").reverse().join(""); // simplified
}

function createUser(name, email, hashedPassword) {
  return { name, email, password: hashedPassword, createdAt: Date.now() };
}

function sendWelcomeEmail(email) {
  console.log(`Sending welcome email to ${email}`);
}

function logRegistration(name) {
  console.log(`Analytics: new user registered — ${name}`);
}

// Orchestrator — composes single-responsibility functions
function processUserRegistration(name, email, password) {
  validateRegistration(name, email, password);
  const hashed = hashPassword(password);
  const user = createUser(name, email, hashed);
  database.push(user);
  sendWelcomeEmail(email);
  logRegistration(name);
  return user;
}

Why SRP matters:

  • Each piece is independently testable.
  • You can reuse hashPassword or validateRegistration elsewhere.
  • Easier to read, debug, and modify one small function at a time.

3. Pure functions

A pure function satisfies two rules:

  1. Deterministic — given the same inputs, it always returns the same output.
  2. No side effects — it does not modify anything outside itself (no global state, no DOM changes, no network calls, no console logs).

Pure

function add(a, b) {
  return a + b;
}

function toUpperCase(str) {
  return str.toUpperCase();
}

function getFullName(first, last) {
  return `${first} ${last}`;
}

// Same input → always same output, nothing else changes
add(2, 3);           // always 5
toUpperCase("hello"); // always "HELLO"

Impure

let total = 0;
function addToTotal(amount) {
  total += amount; // Side effect: modifies external state
  return total;
}

function getRandomGreeting(name) {
  const greetings = ["Hi", "Hello", "Hey"];
  const i = Math.floor(Math.random() * greetings.length);
  return `${greetings[i]}, ${name}`; // Non-deterministic
}

function logMessage(msg) {
  console.log(msg); // Side effect: I/O
}

Why pure functions are valuable

BenefitExplanation
PredictableEasy to reason about — no hidden dependencies
TestableNo mocking needed — just assert input/output
CacheableSame input = same output, so results can be memoized
ParallelizableNo shared state to synchronize
ComposableSafe to chain together

4. Side effects — what they are and when they are acceptable

A side effect is anything a function does besides computing a return value:

Side effectExample
Mutating external variablescount++ where count is outside the function
Writing to the DOMdocument.body.innerHTML = "..."
Network requestsfetch("/api/data")
Console outputconsole.log(...)
Writing to disk / databasefs.writeFileSync(...)
Setting timerssetTimeout(...)
Mutating input argumentsarr.push(item) on a passed-in array

Side effects are not evil — they are how programs interact with the world. The goal is to:

  1. Isolate side effects at the edges of your program (event handlers, API calls).
  2. Keep the core logic pure.
  3. Document when a function has side effects.
// Core logic — PURE
function calculateDiscount(price, percent) {
  return price * (percent / 100);
}

// Edge / glue — has side effects (DOM, user interaction)
function applyDiscountToUI(price, percent) {
  const discount = calculateDiscount(price, percent); // pure call
  document.getElementById("price").textContent = `$${(price - discount).toFixed(2)}`; // side effect
}

5. Function composition

Composition means combining small functions to build more complex behavior, passing the output of one function as the input to the next.

Manual composition

function trim(str) {
  return str.trim();
}

function toLowerCase(str) {
  return str.toLowerCase();
}

function replaceSpaces(str) {
  return str.replace(/\s+/g, "-");
}

// Manual composition — read inside-out
const slug = replaceSpaces(toLowerCase(trim("  Hello World  ")));
console.log(slug); // "hello-world"

Pipe utility (left-to-right)

function pipe(...fns) {
  return function (input) {
    return fns.reduce((value, fn) => fn(value), input);
  };
}

const makeSlug = pipe(trim, toLowerCase, replaceSpaces);
console.log(makeSlug("  Hello World  ")); // "hello-world"

Compose utility (right-to-left — mathematical order)

function compose(...fns) {
  return function (input) {
    return fns.reduceRight((value, fn) => fn(value), input);
  };
}

const makeSlug = compose(replaceSpaces, toLowerCase, trim);
console.log(makeSlug("  Hello World  ")); // "hello-world"

Pipe reads naturally (left-to-right). Compose mirrors mathematical notation f(g(x)). Both are widely used.


6. Higher-order functions

A higher-order function (HOF) is a function that:

  • Takes one or more functions as arguments, OR
  • Returns a function.

HOF that takes a function

function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    action(i);
  }
}

repeat(3, console.log); // 0, 1, 2

repeat(3, (i) => {
  console.log(`Step ${i + 1}`);
});
// Step 1, Step 2, Step 3

HOF that returns a function

function greaterThan(threshold) {
  return function (num) {
    return num > threshold;
  };
}

const isAdult = greaterThan(17);
const isSenior = greaterThan(64);

console.log(isAdult(20));  // true
console.log(isSenior(50)); // false
console.log(isSenior(70)); // true

Built-in higher-order functions

JavaScript arrays have many HOFs built in:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// .filter — takes a predicate function
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]

// .map — takes a transform function
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// .reduce — takes an accumulator function
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 55

// .sort — takes a comparator function
const sorted = [3, 1, 4, 1, 5].sort((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5]

// .forEach — takes an action function
numbers.forEach(n => console.log(n));

7. Callback pattern revisited

A callback is a function passed to another function, to be called at a later time:

function fetchData(url, onSuccess, onError) {
  // Simulated async operation
  setTimeout(() => {
    const success = Math.random() > 0.3;
    if (success) {
      onSuccess({ data: "Sample data from " + url });
    } else {
      onError(new Error("Failed to fetch " + url));
    }
  }, 1000);
}

fetchData(
  "/api/users",
  (result) => console.log("Success:", result),
  (error) => console.error("Error:", error.message)
);

Callback conventions

ConventionExample
Error-first (Node.js style)callback(err, result) — check err first
Separate handlersonSuccess, onError as separate params
Promise-based (modern)Return a Promise instead of accepting callbacks
// Error-first callback (Node.js convention)
function readConfig(path, callback) {
  try {
    const data = { setting: "value" }; // simulated
    callback(null, data);
  } catch (err) {
    callback(err, null);
  }
}

readConfig("/config.json", (err, data) => {
  if (err) {
    console.error("Error:", err);
    return;
  }
  console.log("Config:", data);
});

8. Naming conventions

Good function names communicate intent — use verb-based names that describe the action:

PatternExamplesWhen to use
get*getUser, getName, getTotalRetrieve/compute a value
set*setTheme, setLanguageAssign a value
is* / has* / can*isValid, hasPermission, canEditBoolean check (returns true/false)
create* / make*createUser, makeSlugBuild and return a new thing
calculate* / compute*calculateTax, computeAverageMath/derive a value
handle*handleClick, handleSubmitEvent handler
validate*validateEmail, validateAgeCheck and possibly throw
format*formatCurrency, formatDateTransform for display
parse*parseJSON, parseURLConvert from one format to another
to*toString, toArray, toUpperCaseConvert type/format
on*onClick, onLoadEvent callback (common in React)
fetch* / load*fetchUsers, loadConfigAsync data retrieval

Naming anti-patterns

// BAD — vague
function process(data) { /* ... */ }
function doStuff() { /* ... */ }
function handle(x) { /* ... */ }
function manager() { /* ... */ }

// GOOD — specific
function validateFormData(data) { /* ... */ }
function sendWelcomeEmail() { /* ... */ }
function handleFormSubmit(event) { /* ... */ }
function createUserSession() { /* ... */ }

9. Documentation with JSDoc

JSDoc comments let editors (VS Code) show parameter types, descriptions, and return types as you type:

/**
 * Calculates the total price including tax.
 *
 * @param {number} price - The base price before tax.
 * @param {number} [taxRate=0.08] - The tax rate as a decimal (default 8%).
 * @returns {number} The total price including tax.
 *
 * @example
 * calculateTotal(100);       // 108
 * calculateTotal(100, 0.10); // 110
 */
function calculateTotal(price, taxRate = 0.08) {
  return price + price * taxRate;
}

Common JSDoc tags

TagPurposeExample
@param {type} nameDocument a parameter@param {string} name - The user's name
@param {type} [name=default]Optional parameter with default@param {number} [age=0]
@returns {type}Document return value@returns {boolean}
@throws {ErrorType}Document thrown errors@throws {TypeError}
@exampleProvide usage exampleSee above
@deprecatedMark as deprecated@deprecated Use newFunction instead

JSDoc for callbacks and complex types

/**
 * Filters an array using a predicate function.
 *
 * @param {Array} arr - The array to filter.
 * @param {function(*, number): boolean} predicate - Test function; receives (element, index).
 * @returns {Array} A new array containing elements that passed the test.
 */
function filterArray(arr, predicate) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (predicate(arr[i], i)) {
      result.push(arr[i]);
    }
  }
  return result;
}

10. Real examples — a utility function library

Utility: String helpers

/** Capitalizes the first letter of a string. */
function capitalize(str) {
  if (!str) return "";
  return str[0].toUpperCase() + str.slice(1);
}

/** Converts a string to kebab-case. */
function toKebabCase(str) {
  return str
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "-")
    .replace(/[^a-z0-9-]/g, "");
}

/** Truncates a string to maxLength, adding "..." if truncated. */
function truncate(str, maxLength = 50) {
  if (str.length <= maxLength) return str;
  return str.slice(0, maxLength - 3) + "...";
}

Utility: Validation helpers

/** @returns {boolean} True if the email format looks valid. */
function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

/** @returns {boolean} True if the value is a non-negative finite number. */
function isPositiveNumber(value) {
  return typeof value === "number" && isFinite(value) && value >= 0;
}

/** @returns {boolean} True if the string is non-empty after trimming. */
function isNonEmpty(str) {
  return typeof str === "string" && str.trim().length > 0;
}

Utility: Array helpers (higher-order)

/** Groups array items by a key-producing function. */
function groupBy(arr, keyFn) {
  return arr.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) groups[key] = [];
    groups[key].push(item);
    return groups;
  }, {});
}

// Usage
const people = [
  { name: "Alice", dept: "Engineering" },
  { name: "Bob", dept: "Marketing" },
  { name: "Carol", dept: "Engineering" },
];

console.log(groupBy(people, p => p.dept));
// { Engineering: [{...}, {...}], Marketing: [{...}] }

Key takeaways

  1. DRY — extract repeated logic into functions. One bug fix, one place.
  2. Single Responsibility — each function does one thing. Compose them for complex workflows.
  3. Pure functions are deterministic and side-effect-free — predictable, testable, cacheable.
  4. Side effects are necessary but should be isolated at program edges, not buried in core logic.
  5. Composition (pipe/compose) chains small functions into powerful pipelines.
  6. Higher-order functions accept or return functions — the backbone of .map, .filter, .reduce, and custom utilities.
  7. Callbacks pass a function to be called later — the original async pattern in JavaScript.
  8. Name functions with verbs that describe the action: getUser, isValid, formatDate.
  9. JSDoc communicates types and intent — your editor uses it for inline help.

Explain-It Challenge

Explain without notes:

  1. What makes a function pure? Give one example of a pure function and one impure function.
  2. What is a higher-order function? Name two built-in JavaScript HOFs.
  3. Why should core business logic be separated from side effects?
  4. What does the pipe function do and why is it useful?

Navigation: ← 1.20.d — Arrow Functions · 1.20.f — Practice Problems →