Skip to content

Functions

Functions are reusable blocks of code that perform specific tasks. They are fundamental to JavaScript programming.

What You'll Learn

  • How to create and call functions
  • Different ways to define functions
  • Parameters, arguments, and return values
  • Scope and closures
  • Higher-order functions

What is a Function?

A function is like a recipe - a set of instructions you can use over and over again.

┌─────────────────────────────────────────────────────────────┐
│                     FUNCTION ANATOMY                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   function greet(name) {         ← Function declaration     │
│            ─────  ────                                      │
│              │      │                                       │
│              │      └─ Parameter (input placeholder)        │
│              │                                              │
│              └─ Function name                               │
│                                                             │
│       return `Hello, ${name}!`;  ← Return statement        │
│   }                                (output)                 │
│                                                             │
│   greet("Alice");                ← Function call            │
│         ───────                                             │
│            │                                                │
│            └─ Argument (actual value passed)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Function Declaration

The traditional and most common way to define a function.

js
// Define the function
function greet(name) {
    return `Hello, ${name}!`;
}

// Call (use) the function
console.log(greet("Alice")); // "Hello, Alice!"
console.log(greet("Bob"));   // "Hello, Bob!"

Multiple Parameters

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

console.log(add(5, 3));   // 8
console.log(add(10, 20)); // 30

No Parameters

js
function sayHello() {
    return "Hello, World!";
}

console.log(sayHello()); // "Hello, World!"

No Return (Side Effects)

js
function logMessage(message) {
    console.log(message);
    // No return statement
    // Implicitly returns undefined
}

const result = logMessage("Hello");
console.log(result); // undefined

When to Use Return

┌─────────────────────────────────────────────────────────────┐
│            WITH RETURN          │      WITHOUT RETURN       │
├─────────────────────────────────────────────────────────────┤
│                                 │                           │
│  Calculate and give back       │  Do something (side       │
│  a value                        │  effect) - print, save,   │
│                                 │  modify, etc.             │
│                                 │                           │
│  function add(a, b) {          │  function logTime() {     │
│      return a + b;             │      console.log(Date()); │
│  }                              │  }                        │
│                                 │                           │
│  const sum = add(2, 3);        │  logTime(); // just runs  │
│  // sum = 5                     │                           │
│                                 │                           │
└─────────────────────────────────────────────────────────────┘

Function Expression

Assign a function to a variable. The function can be anonymous (no name).

js
// Anonymous function expression
const multiply = function(a, b) {
    return a * b;
};

console.log(multiply(4, 5)); // 20

// Named function expression (useful for recursion/debugging)
const factorial = function fact(n) {
    if (n <= 1) return 1;
    return n * fact(n - 1);
};

Declaration vs Expression

FeatureDeclarationExpression
Syntaxfunction name() {}const name = function() {}
Hoisting✅ Hoisted (can call before definition)❌ Not hoisted
NameRequiredOptional
js
// Declaration - can call before it's defined (hoisting)
sayHi(); // Works! "Hi!"
function sayHi() {
    console.log("Hi!");
}

// Expression - cannot call before it's defined
// sayBye(); // Error! Cannot access before initialization
const sayBye = function() {
    console.log("Bye!");
};
sayBye(); // Works after definition

Arrow Functions (ES6)

A shorter, more concise syntax for writing functions.

js
// Regular function
const add = function(a, b) {
    return a + b;
};

// Arrow function equivalent
const addArrow = (a, b) => {
    return a + b;
};

// Even shorter: implicit return (single expression)
const addShort = (a, b) => a + b;

Arrow Function Syntax Rules

js
// Multiple parameters - parentheses required
const add = (a, b) => a + b;

// Single parameter - parentheses optional
const double = n => n * 2;
const doubleAlt = (n) => n * 2;  // Also valid

// No parameters - empty parentheses required
const sayHello = () => "Hello!";

// Multiple lines - curly braces and return required
const greet = (name) => {
    const greeting = `Hello, ${name}!`;
    return greeting;
};

// Returning an object - wrap in parentheses
const createUser = (name, age) => ({ name, age });
// Without parentheses, {} would be seen as function body

Arrow Function Cheat Sheet

┌─────────────────────────────────────────────────────────────┐
│                  ARROW FUNCTION SHORTCUTS                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Parameters:                                                │
│  ───────────                                                │
│  ()          →  No parameters                               │
│  x           →  One parameter (no parentheses needed)       │
│  (x, y)      →  Multiple parameters                         │
│                                                             │
│  Body:                                                      │
│  ─────                                                      │
│  => x        →  Single expression (implicit return)         │
│  => { }      →  Multiple statements (explicit return)       │
│  => ({ })    →  Return object literal                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

When to Use Arrow Functions

  • Short, simple functions
  • Callbacks (array methods, event handlers)
  • When you need to preserve this context

When NOT to use:

  • Object methods (use regular functions for correct this)
  • When you need the arguments object
  • Constructors (can't use new with arrow functions)

Parameters and Arguments

Parameters vs Arguments

js
//                    parameters (placeholders)
//                         ↓     ↓
function greet(firstName, lastName) {
    return `Hello, ${firstName} ${lastName}!`;
}

//              arguments (actual values)
//                  ↓       ↓
greet("John", "Doe");

Default Parameters

Provide fallback values when arguments aren't passed.

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

console.log(greet());        // "Hello, Guest!"
console.log(greet("Alice")); // "Hello, Alice!"

// Multiple defaults
function createUser(name = "Anonymous", role = "user", active = true) {
    return { name, role, active };
}

console.log(createUser());                    // { name: "Anonymous", role: "user", active: true }
console.log(createUser("John"));              // { name: "John", role: "user", active: true }
console.log(createUser("John", "admin"));     // { name: "John", role: "admin", active: true }

Rest Parameters

Collect any number of arguments into an array.

js
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2));           // 3
console.log(sum(1, 2, 3, 4, 5));  // 15

// Combining with regular parameters
function introduce(greeting, ...names) {
    return `${greeting}, ${names.join(" and ")}!`;
}

console.log(introduce("Hello", "Alice", "Bob", "Charlie"));
// "Hello, Alice and Bob and Charlie!"

Destructuring Parameters

Extract values from objects/arrays in parameters.

js
// Object destructuring
function printUser({ name, age, city = "Unknown" }) {
    console.log(`${name}, ${age}, from ${city}`);
}

printUser({ name: "Alice", age: 25, city: "NYC" });
// "Alice, 25, from NYC"

printUser({ name: "Bob", age: 30 });
// "Bob, 30, from Unknown"

// Array destructuring
function getFirstTwo([first, second]) {
    return `First: ${first}, Second: ${second}`;
}

getFirstTwo([1, 2, 3, 4]); // "First: 1, Second: 2"

Scope

Scope determines where variables are accessible.

Visual Guide to Scope

┌──────────────────────────────────────────────────────────────┐
│  GLOBAL SCOPE                                                │
│  ─────────────                                               │
│  const globalVar = "I'm everywhere!";                        │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  FUNCTION SCOPE (myFunction)                            │ │
│  │  ──────────────                                         │ │
│  │  const functionVar = "I'm in this function";            │ │
│  │  // Can access: globalVar, functionVar                  │ │
│  │                                                         │ │
│  │  ┌───────────────────────────────────────────────────┐  │ │
│  │  │  BLOCK SCOPE (if, for, etc.)                      │  │ │
│  │  │  ───────────                                      │  │ │
│  │  │  const blockVar = "I'm only in this block";       │  │ │
│  │  │  // Can access: globalVar, functionVar, blockVar  │  │ │
│  │  └───────────────────────────────────────────────────┘  │ │
│  │  // Cannot access blockVar here!                        │ │
│  └─────────────────────────────────────────────────────────┘ │
│  // Cannot access functionVar or blockVar here!              │
└──────────────────────────────────────────────────────────────┘

Global Scope

Variables declared outside any function.

js
const globalVar = "I'm global";

function showGlobal() {
    console.log(globalVar); // ✅ Accessible
}

showGlobal(); // "I'm global"
console.log(globalVar); // ✅ Accessible everywhere

Function Scope

Variables declared inside a function are only accessible within that function.

js
function myFunction() {
    const localVar = "I'm local";
    console.log(localVar); // ✅ Accessible
}

myFunction();
// console.log(localVar); // ❌ Error: localVar is not defined

Block Scope (let and const)

Variables declared with let and const are limited to their block {}.

js
if (true) {
    let blockLet = "Block scoped with let";
    const blockConst = "Block scoped with const";
    var notBlockScoped = "Function scoped with var";

    console.log(blockLet);   // ✅ Accessible
    console.log(blockConst); // ✅ Accessible
}

// console.log(blockLet);   // ❌ Error
// console.log(blockConst); // ❌ Error
console.log(notBlockScoped); // ✅ "Function scoped with var" - var ignores blocks!

Avoid var

Use let and const instead of var. The var keyword doesn't respect block scope, which can cause bugs.

Closures

A closure is a function that "remembers" variables from its outer scope, even after the outer function has finished running.

Simple Closure Example

js
function createCounter() {
    let count = 0;  // This variable is "enclosed"

    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// Each call to createCounter() creates a new, separate closure
const counter2 = createCounter();
console.log(counter2()); // 1 (separate count)

How Closures Work

┌─────────────────────────────────────────────────────────────┐
│  createCounter() runs:                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  let count = 0                                       │   │
│  │                                                      │   │
│  │  Returns inner function ─────┐                       │   │
│  └──────────────────────────────┼───────────────────────┘   │
│                                 │                           │
│  createCounter() finishes...    │                           │
│  BUT count is NOT garbage       │                           │
│  collected because...           │                           │
│                                 ▼                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  The returned function       │                       │   │
│  │  still has a reference ──────┘                       │   │
│  │  to count (closure!)                                 │   │
│  │                                                      │   │
│  │  counter() → count++ → return count                  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Practical Closure Examples

js
// Create private variables
function createBankAccount(initialBalance) {
    let balance = initialBalance;  // Private!

    return {
        deposit: (amount) => {
            balance += amount;
            return balance;
        },
        withdraw: (amount) => {
            if (amount > balance) {
                return "Insufficient funds";
            }
            balance -= amount;
            return balance;
        },
        getBalance: () => balance
    };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50));  // 150
console.log(account.withdraw(30)); // 120
// console.log(account.balance);   // undefined - can't access directly!

// Create function factories
function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);

console.log(double(5));    // 10
console.log(triple(5));    // 15
console.log(tenTimes(5));  // 50

Higher-Order Functions

Functions that take other functions as arguments or return functions.

Functions as Arguments (Callbacks)

js
// Custom higher-order function
function processArray(arr, callback) {
    const result = [];
    for (const item of arr) {
        result.push(callback(item));
    }
    return result;
}

const numbers = [1, 2, 3, 4, 5];

// Pass different functions for different behaviors
const doubled = processArray(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const squared = processArray(numbers, n => n * n);
console.log(squared); // [1, 4, 9, 16, 25]

const asStrings = processArray(numbers, n => `Number: ${n}`);
console.log(asStrings); // ["Number: 1", "Number: 2", ...]

Functions as Return Values

js
function createGreeter(greeting) {
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeter("Hello");
const sayGoodbye = createGreeter("Goodbye");
const sayHi = createGreeter("Hi there");

console.log(sayHello("Alice"));   // "Hello, Alice!"
console.log(sayGoodbye("Bob"));   // "Goodbye, Bob!"
console.log(sayHi("Charlie"));    // "Hi there, Charlie!"

Common Higher-Order Functions

js
const numbers = [1, 2, 3, 4, 5];

// map - transform each element
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]

// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]

// reduce - combine into single value
const sum = numbers.reduce((total, n) => total + n, 0);
// 15

// find - get first matching element
const firstEven = numbers.find(n => n % 2 === 0);
// 2

// every - check if ALL pass test
const allPositive = numbers.every(n => n > 0);
// true

// some - check if ANY pass test
const hasNegative = numbers.some(n => n < 0);
// false

The this Keyword

The value of this depends on how a function is called.

this in Different Contexts

js
// In an object method - this = the object
const person = {
    name: "Alice",
    greet: function() {
        console.log(`Hello, I'm ${this.name}`);
    }
};
person.greet(); // "Hello, I'm Alice"

// Regular function (non-strict) - this = window/global
function showThis() {
    console.log(this);
}
showThis(); // Window object (browser) or global (Node)

// Arrow function - this = surrounding scope's this
const obj = {
    name: "Bob",
    greetRegular: function() {
        console.log(`Regular: ${this.name}`);
    },
    greetArrow: () => {
        console.log(`Arrow: ${this.name}`); // this is NOT obj!
    }
};
obj.greetRegular(); // "Regular: Bob"
obj.greetArrow();   // "Arrow: undefined"

Binding this Explicitly

js
function greet() {
    console.log(`Hello, ${this.name}`);
}

const person1 = { name: "Alice" };
const person2 = { name: "Bob" };

// call - invoke immediately with specified 'this'
greet.call(person1);  // "Hello, Alice"
greet.call(person2);  // "Hello, Bob"

// apply - like call, but pass arguments as array
function introduce(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
introduce.apply(person1, ["Hi", "!"]); // "Hi, I'm Alice!"

// bind - create new function with bound 'this'
const greetAlice = greet.bind(person1);
greetAlice(); // "Hello, Alice"

Common this Problem and Solution

js
const user = {
    name: "Alice",
    friends: ["Bob", "Charlie"],

    // ❌ Problem: this is lost in callback
    listFriendsBroken: function() {
        this.friends.forEach(function(friend) {
            console.log(`${this.name} is friends with ${friend}`);
            // this.name is undefined!
        });
    },

    // ✅ Solution 1: Arrow function (inherits this)
    listFriendsArrow: function() {
        this.friends.forEach((friend) => {
            console.log(`${this.name} is friends with ${friend}`);
        });
    },

    // ✅ Solution 2: Save this in a variable
    listFriendsSaved: function() {
        const self = this;
        this.friends.forEach(function(friend) {
            console.log(`${self.name} is friends with ${friend}`);
        });
    },

    // ✅ Solution 3: Use bind
    listFriendsBind: function() {
        this.friends.forEach(function(friend) {
            console.log(`${this.name} is friends with ${friend}`);
        }.bind(this));
    }
};

IIFE (Immediately Invoked Function Expression)

A function that runs immediately after being defined.

js
// Basic IIFE
(function() {
    const private = "I'm private to this IIFE";
    console.log(private);
})();

// Arrow function IIFE
(() => {
    console.log("Arrow IIFE!");
})();

// IIFE with parameters
((name) => {
    console.log(`Hello, ${name}!`);
})("World");

// IIFE returning a value
const result = (() => {
    const a = 10;
    const b = 20;
    return a + b;
})();
console.log(result); // 30

Why Use IIFE?

js
// Create private scope to avoid polluting global namespace
const counter = (function() {
    let count = 0;  // Private variable

    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount: () => count
    };
})();

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
// count is not accessible from outside!

Recursion

A function that calls itself. Must have a base case to stop!

js
// Countdown
function countdown(n) {
    if (n <= 0) {           // Base case - when to stop
        console.log("Done!");
        return;
    }
    console.log(n);
    countdown(n - 1);       // Recursive call
}

countdown(5);
// 5, 4, 3, 2, 1, Done!

// Factorial: n! = n × (n-1) × (n-2) × ... × 1
function factorial(n) {
    if (n <= 1) return 1;   // Base case
    return n * factorial(n - 1);
}

console.log(factorial(5)); // 120 (5 × 4 × 3 × 2 × 1)

Recursion Visualization

factorial(5)
├── 5 * factorial(4)
│   ├── 4 * factorial(3)
│   │   ├── 3 * factorial(2)
│   │   │   ├── 2 * factorial(1)
│   │   │   │   └── 1  (base case!)
│   │   │   └── 2 * 1 = 2
│   │   └── 3 * 2 = 6
│   └── 4 * 6 = 24
└── 5 * 24 = 120

Always Include a Base Case!

Without a base case, recursion will continue forever (stack overflow).

js
// ❌ Bad - infinite recursion
function badRecursion(n) {
    return badRecursion(n + 1); // Never stops!
}

Exercises

Exercise 1: Simple Calculator

Create a calculator with functions for add, subtract, multiply, divide.

Solution
js
const calculator = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => {
        if (b === 0) return "Cannot divide by zero";
        return a / b;
    }
};

console.log(calculator.add(10, 5));      // 15
console.log(calculator.subtract(10, 5)); // 5
console.log(calculator.multiply(10, 5)); // 50
console.log(calculator.divide(10, 5));   // 2
console.log(calculator.divide(10, 0));   // "Cannot divide by zero"

Exercise 2: Function Composition

Create a function that composes two functions together.

Solution
js
function compose(f, g) {
    return function(x) {
        return f(g(x));  // Apply g first, then f
    };
}

const addOne = x => x + 1;
const double = x => x * 2;

const addOneThenDouble = compose(double, addOne);
const doubleThenAddOne = compose(addOne, double);

console.log(addOneThenDouble(5)); // 12 ((5 + 1) * 2)
console.log(doubleThenAddOne(5)); // 11 ((5 * 2) + 1)

Exercise 3: Memoization

Create a memoized function that caches results.

Solution
js
function memoize(fn) {
    const cache = {};

    return function(...args) {
        const key = JSON.stringify(args);

        if (key in cache) {
            console.log("From cache:", key);
            return cache[key];
        }

        console.log("Computing:", key);
        const result = fn.apply(this, args);
        cache[key] = result;
        return result;
    };
}

// Example: expensive fibonacci calculation
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // Computing (first time)
console.log(fibonacci(10)); // From cache (instant!)

Exercise 4: Debounce Function

Create a debounce function that limits how often a function can run.

Solution
js
function debounce(fn, delay) {
    let timeoutId;

    return function(...args) {
        // Clear previous timeout
        clearTimeout(timeoutId);

        // Set new timeout
        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// Usage: only runs after user stops typing for 300ms
const handleSearch = debounce((query) => {
    console.log("Searching for:", query);
}, 300);

// Simulating rapid typing
handleSearch("h");
handleSearch("he");
handleSearch("hel");
handleSearch("hell");
handleSearch("hello");
// Only "Searching for: hello" will print (after 300ms)

Common Mistakes and Pitfalls

Avoid These Function Mistakes

1. Losing this Context

js
const user = {
    name: "Alice",
    greet() {
        console.log(`Hello, ${this.name}`);
    }
};

// ❌ Lost context when passing as callback
setTimeout(user.greet, 1000); // "Hello, undefined"

// ✅ Fix with bind
setTimeout(user.greet.bind(user), 1000); // "Hello, Alice"

// ✅ Or wrap in arrow function
setTimeout(() => user.greet(), 1000); // "Hello, Alice"

2. Arrow Functions for Object Methods

js
const counter = {
    count: 0,
    // ❌ Arrow function doesn't have its own 'this'
    incrementBad: () => {
        this.count++; // 'this' is not the counter object!
    },
    // ✅ Use regular function for methods
    incrementGood() {
        this.count++;
    }
};

3. Forgetting Return in Arrow Functions

js
// ❌ No return with curly braces
const double = x => { x * 2 };
console.log(double(5)); // undefined

// ✅ With curly braces, need explicit return
const doubleFixed = x => { return x * 2 };

// ✅ Or omit curly braces for implicit return
const doubleBest = x => x * 2;

4. Infinite Recursion

js
// ❌ Missing or wrong base case
function badRecursion(n) {
    return n + badRecursion(n - 1); // Never stops!
}

// ✅ Always have a proper base case
function goodRecursion(n) {
    if (n <= 0) return 0; // Base case
    return n + goodRecursion(n - 1);
}

Interview Questions

Q: What is a closure and why is it useful?

Answer: A closure is a function that has access to variables from its outer (enclosing) scope, even after that outer function has returned.

Use cases:

  • Data privacy (module pattern)
  • Function factories
  • Maintaining state in async operations
  • Partial application and currying
js
function createCounter() {
    let count = 0; // Private variable
    return {
        increment: () => ++count,
        getCount: () => count
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
// count is not directly accessible
Q: What's the difference between call, apply, and bind?

Answer:

MethodWhen it runsArgumentsReturns
callImmediatelyList: a, b, cFunction result
applyImmediatelyArray: [a, b, c]Function result
bindLater (when called)List: a, b, cNew function
js
function greet(greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: "Alice" };

// call - runs immediately, args as list
greet.call(person, "Hello", "!"); // "Hello, Alice!"

// apply - runs immediately, args as array
greet.apply(person, ["Hi", "?"]); // "Hi, Alice?"

// bind - returns new function for later
const boundGreet = greet.bind(person, "Hey");
boundGreet("!"); // "Hey, Alice!"
Q: What is function hoisting?

Answer: Function declarations are hoisted completely, meaning you can call them before they appear in the code.

Function expressions (including arrow functions) are NOT hoisted - they follow variable hoisting rules.

js
// Works - function declaration is hoisted
sayHello(); // "Hello!"
function sayHello() {
    console.log("Hello!");
}

// Error - function expression not hoisted
// sayGoodbye(); // TypeError: sayGoodbye is not a function
const sayGoodbye = function() {
    console.log("Goodbye!");
};

Real-World Examples

API Request Handler

js
// Reusable API handler with error handling
async function apiRequest(url, options = {}) {
    const defaultOptions = {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
        },
        timeout: 5000,
    };

    const config = { ...defaultOptions, ...options };

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), config.timeout);

    try {
        const response = await fetch(url, {
            ...config,
            signal: controller.signal,
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        return await response.json();
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === 'AbortError') {
            throw new Error('Request timed out');
        }
        throw error;
    }
}

// Usage
async function getUser(id) {
    return apiRequest(`/api/users/${id}`);
}

async function createUser(userData) {
    return apiRequest('/api/users', {
        method: 'POST',
        body: JSON.stringify(userData),
    });
}

Event Handler Factory

js
// Create event handlers with configurable behavior
function createClickHandler(config) {
    const {
        preventDefault = true,
        stopPropagation = false,
        debounceMs = 0,
        onSuccess,
        onError,
    } = config;

    let timeoutId;

    return function handleClick(event) {
        if (preventDefault) event.preventDefault();
        if (stopPropagation) event.stopPropagation();

        const execute = async () => {
            try {
                await config.action(event);
                onSuccess?.();
            } catch (error) {
                onError?.(error);
            }
        };

        if (debounceMs > 0) {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(execute, debounceMs);
        } else {
            execute();
        }
    };
}

// Usage
const submitHandler = createClickHandler({
    action: async (e) => {
        const form = e.target.closest('form');
        const data = new FormData(form);
        await fetch('/api/submit', {
            method: 'POST',
            body: data,
        });
    },
    debounceMs: 300,
    onSuccess: () => console.log('Submitted!'),
    onError: (err) => console.error('Failed:', err),
});

button.addEventListener('click', submitHandler);

Validation Pipeline

js
// Composable validation functions
const validators = {
    required: (fieldName) => (value) => {
        if (!value || value.trim() === '') {
            return `${fieldName} is required`;
        }
        return null;
    },

    minLength: (min) => (value) => {
        if (value && value.length < min) {
            return `Must be at least ${min} characters`;
        }
        return null;
    },

    maxLength: (max) => (value) => {
        if (value && value.length > max) {
            return `Must be no more than ${max} characters`;
        }
        return null;
    },

    email: () => (value) => {
        const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (value && !pattern.test(value)) {
            return 'Invalid email format';
        }
        return null;
    },

    matches: (pattern, message) => (value) => {
        if (value && !pattern.test(value)) {
            return message;
        }
        return null;
    },
};

// Combine validators
function createValidator(...validatorFns) {
    return function validate(value) {
        for (const validator of validatorFns) {
            const error = validator(value);
            if (error) return error;
        }
        return null;
    };
}

// Usage
const validateUsername = createValidator(
    validators.required('Username'),
    validators.minLength(3),
    validators.maxLength(20),
    validators.matches(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores')
);

const validateEmail = createValidator(
    validators.required('Email'),
    validators.email()
);

console.log(validateUsername('')); // "Username is required"
console.log(validateUsername('ab')); // "Must be at least 3 characters"
console.log(validateUsername('valid_user')); // null (valid)

Summary

ConceptDescriptionExample
DeclarationTraditional function syntax, hoistedfunction name() {}
ExpressionAssigned to variable, not hoistedconst fn = function() {}
Arrow FunctionConcise syntax, lexical thisconst fn = () => {}
Default ParametersFallback valuesfunction(x = 10)
Rest ParametersCollect argumentsfunction(...args)
ScopeVariable accessibilityGlobal, function, block
ClosureFunction remembering outer scopeCounter example
Higher-OrderFunctions with function args/returnsmap, filter, reduce
thisContext-dependent keywordVaries by call method
IIFEImmediately invoked(() => {})()
RecursionFunction calling itselffactorial(n)

Next Steps

Continue to Arrays to learn about working with collections of data.