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
calculateTotalonce, 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
hashPasswordorvalidateRegistrationelsewhere. - Easier to read, debug, and modify one small function at a time.
3. Pure functions
A pure function satisfies two rules:
- Deterministic — given the same inputs, it always returns the same output.
- 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
| Benefit | Explanation |
|---|---|
| Predictable | Easy to reason about — no hidden dependencies |
| Testable | No mocking needed — just assert input/output |
| Cacheable | Same input = same output, so results can be memoized |
| Parallelizable | No shared state to synchronize |
| Composable | Safe 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 effect | Example |
|---|---|
| Mutating external variables | count++ where count is outside the function |
| Writing to the DOM | document.body.innerHTML = "..." |
| Network requests | fetch("/api/data") |
| Console output | console.log(...) |
| Writing to disk / database | fs.writeFileSync(...) |
| Setting timers | setTimeout(...) |
| Mutating input arguments | arr.push(item) on a passed-in array |
Side effects are not evil — they are how programs interact with the world. The goal is to:
- Isolate side effects at the edges of your program (event handlers, API calls).
- Keep the core logic pure.
- 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
| Convention | Example |
|---|---|
| Error-first (Node.js style) | callback(err, result) — check err first |
| Separate handlers | onSuccess, 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:
| Pattern | Examples | When to use |
|---|---|---|
get* | getUser, getName, getTotal | Retrieve/compute a value |
set* | setTheme, setLanguage | Assign a value |
is* / has* / can* | isValid, hasPermission, canEdit | Boolean check (returns true/false) |
create* / make* | createUser, makeSlug | Build and return a new thing |
calculate* / compute* | calculateTax, computeAverage | Math/derive a value |
handle* | handleClick, handleSubmit | Event handler |
validate* | validateEmail, validateAge | Check and possibly throw |
format* | formatCurrency, formatDate | Transform for display |
parse* | parseJSON, parseURL | Convert from one format to another |
to* | toString, toArray, toUpperCase | Convert type/format |
on* | onClick, onLoad | Event callback (common in React) |
fetch* / load* | fetchUsers, loadConfig | Async 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
| Tag | Purpose | Example |
|---|---|---|
@param {type} name | Document 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} |
@example | Provide usage example | See above |
@deprecated | Mark 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
- DRY — extract repeated logic into functions. One bug fix, one place.
- Single Responsibility — each function does one thing. Compose them for complex workflows.
- Pure functions are deterministic and side-effect-free — predictable, testable, cacheable.
- Side effects are necessary but should be isolated at program edges, not buried in core logic.
- Composition (pipe/compose) chains small functions into powerful pipelines.
- Higher-order functions accept or return functions — the backbone of
.map,.filter,.reduce, and custom utilities. - Callbacks pass a function to be called later — the original async pattern in JavaScript.
- Name functions with verbs that describe the action:
getUser,isValid,formatDate. - JSDoc communicates types and intent — your editor uses it for inline help.
Explain-It Challenge
Explain without notes:
- What makes a function pure? Give one example of a pure function and one impure function.
- What is a higher-order function? Name two built-in JavaScript HOFs.
- Why should core business logic be separated from side effects?
- What does the
pipefunction do and why is it useful?
Navigation: ← 1.20.d — Arrow Functions · 1.20.f — Practice Problems →