Functions Are Values
In JavaScript, functions are first-class citizens. This means they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures — just like numbers or strings.
// Assign to a variable
const greet = function (name) {
return `Hello, ${name}!`;
};
// Store in an array
const operations = [
(x) => x + 1,
(x) => x * 2,
(x) => x ** 2,
];
// Store in an object
const math = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
};
// Pass as argument
console.log([3, 1, 2].sort((a, b) => a - b)); // [1, 2, 3]
This property of functions is what makes callbacks and higher-order functions possible.
What Is a Callback?
A callback is a function passed as an argument to another function, to be called (or "called back") at a later time.
function fetchData(url, onSuccess, onError) {
// simulate async operation
setTimeout(() => {
if (url.startsWith('https')) {
onSuccess({ data: 'some data' }); // callback on success
} else {
onError(new Error('Invalid URL')); // callback on error
}
}, 1000);
}
// Passing callbacks
fetchData(
'https://api.example.com',
(result) => console.log('Got:', result),
(error) => console.error('Failed:', error.message)
);
Synchronous Callbacks
Not all callbacks are async. Many are called immediately.
// Array methods use synchronous callbacks
const numbers = [1, 2, 3, 4, 5];
// forEach — callback runs for each element
numbers.forEach((num) => {
console.log(num * 2);
});
// 2, 4, 6, 8, 10
// sort — callback compares elements
const sorted = [3, 1, 4, 1, 5].sort((a, b) => a - b);
console.log(sorted); // [1, 1, 3, 4, 5]
// String replace — callback transforms matches
'hello world'.replace(/\b\w/g, (char) => char.toUpperCase());
// 'Hello World'
Asynchronous Callbacks
Async callbacks run after some operation completes (network request, timer, file read, etc.).
// setTimeout — callback runs after delay
setTimeout(() => {
console.log('Runs after 1 second');
}, 1000);
// Event listener — callback runs when event fires
document.addEventListener('click', (event) => {
console.log(`Clicked at ${event.clientX}, ${event.clientY}`);
});
// Node.js fs — callback runs when file is read
const fs = require('fs');
fs.readFile('data.json', 'utf8', (err, data) => {
if (err) {
console.error('Read failed:', err.message);
return;
}
console.log('File contents:', data);
});
Callback Execution Timeline
Synchronous callback:
main() ─────> forEach() ─────> callback() ─────> next line
runs immediately
Asynchronous callback:
main() ─────> setTimeout(callback, 1000) ─────> next line
|
└──── 1000ms later ──── callback() runs
Inversion of Control
When you pass a callback, you hand over control of when and how it gets called. This is called inversion of control — the calling code decides the execution details.
// You control when this runs
processData();
// The library controls when this runs
thirdPartyLib.onReady(() => {
processData(); // you trust the library to call this correctly
});
The Trust Problem
With callbacks, you trust the receiver to:
- Call the callback the right number of times (once, not zero, not five)
- Pass the correct arguments
- Call it at the right time
- Handle errors properly
// What if the library calls your callback multiple times?
paymentService.charge(amount, () => {
sendConfirmationEmail(); // might send 3 emails if called 3 times
updateDatabase(); // might corrupt data
});
This trust problem is one reason Promises and async/await were created — they provide guarantees that callbacks cannot.
What Is a Higher-Order Function?
A higher-order function (HOF) is a function that either:
- Takes a function as an argument (accepts a callback), or
- Returns a function, or
- Both
// Takes a function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Iteration ${i}`));
// Iteration 0
// Iteration 1
// Iteration 2
// Returns a function
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Both: takes and returns a function
function logging(fn) {
return function (...args) {
console.log(`Calling with args: ${args}`);
const result = fn(...args);
console.log(`Result: ${result}`);
return result;
};
}
const loggedAdd = logging((a, b) => a + b);
loggedAdd(2, 3);
// Calling with args: 2,3
// Result: 5
map, filter, reduce — The Core HOFs
These three array methods are the foundation of functional programming in JavaScript.
const users = [
{ name: 'Alice', age: 28, active: true },
{ name: 'Bob', age: 35, active: false },
{ name: 'Charlie', age: 22, active: true },
{ name: 'Diana', age: 31, active: true },
];
// map — transform each element
const names = users.map((user) => user.name);
console.log(names); // ['Alice', 'Bob', 'Charlie', 'Diana']
// filter — keep elements that match
const activeUsers = users.filter((user) => user.active);
console.log(activeUsers.length); // 3
// reduce — combine elements into single value
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
console.log(totalAge); // 116
// Chain them together
const activeNames = users
.filter((user) => user.active)
.map((user) => user.name)
.join(', ');
console.log(activeNames); // 'Alice, Charlie, Diana'
Building Your Own HOFs
// Custom filter
function myFilter(array, predicate) {
const result = [];
for (const item of array) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
const evens = myFilter([1, 2, 3, 4, 5], (n) => n % 2 === 0);
console.log(evens); // [2, 4]
// Custom map
function myMap(array, transform) {
const result = [];
for (const item of array) {
result.push(transform(item));
}
return result;
}
const doubled = myMap([1, 2, 3], (n) => n * 2);
console.log(doubled); // [2, 4, 6]
// Custom reduce
function myReduce(array, reducer, initialValue) {
let accumulator = initialValue;
for (const item of array) {
accumulator = reducer(accumulator, item);
}
return accumulator;
}
const sum = myReduce([1, 2, 3, 4], (acc, n) => acc + n, 0);
console.log(sum); // 10
Function Composition
Function composition combines simple functions to build more complex ones. The output of one function becomes the input of the next.
// Without composition — nested calls
const result = toUpperCase(trim(removeSpecialChars(input)));
// With composition — readable pipeline
const sanitize = compose(toUpperCase, trim, removeSpecialChars);
const result = sanitize(input);
compose and pipe
// compose — right to left (mathematical convention)
function compose(...fns) {
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
}
// pipe — left to right (reading order)
function pipe(...fns) {
return (value) => fns.reduce((acc, fn) => fn(acc), value);
}
// Example functions
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;
// compose: square(double(addOne(3))) = square(double(4)) = square(8) = 64
const composed = compose(square, double, addOne);
console.log(composed(3)); // 64
// pipe: addOne(3) = 4, double(4) = 8, square(8) = 64
const piped = pipe(addOne, double, square);
console.log(piped(3)); // 64
Practical Composition
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
// String processing pipeline
const slugify = pipe(
(str) => str.toLowerCase(),
(str) => str.trim(),
(str) => str.replace(/[^\w\s-]/g, ''),
(str) => str.replace(/\s+/g, '-'),
(str) => str.replace(/-+/g, '-'),
);
console.log(slugify(' Hello World! This is a Test '));
// 'hello-world-this-is-a-test'
// Data processing pipeline
const processUsers = pipe(
(users) => users.filter((u) => u.active),
(users) => users.map((u) => ({ ...u, name: u.name.toUpperCase() })),
(users) => users.sort((a, b) => a.age - b.age),
);
const result = processUsers([
{ name: 'Charlie', age: 22, active: true },
{ name: 'Alice', age: 28, active: true },
{ name: 'Bob', age: 35, active: false },
]);
// [{ name: 'CHARLIE', age: 22 }, { name: 'ALICE', age: 28 }]
Currying
Currying transforms a function that takes multiple arguments into a series of functions that each take one argument.
// Normal function
function add(a, b) {
return a + b;
}
add(2, 3); // 5
// Curried version
function curriedAdd(a) {
return function (b) {
return a + b;
};
}
curriedAdd(2)(3); // 5
// Arrow function syntax
const curriedAdd = (a) => (b) => a + b;
curriedAdd(2)(3); // 5
Why Curry?
Currying lets you create specialized functions from general ones.
// General formatter
const format = (prefix) => (suffix) => (value) =>
`${prefix}${value}${suffix}`;
// Specialized formatters
const wrapInParens = format('(')(')');
const wrapInBrackets = format('[')(']');
const addDollar = format('$')('');
console.log(wrapInParens('hello')); // (hello)
console.log(wrapInBrackets('hello')); // [hello]
console.log(addDollar('9.99')); // $9.99
// Combine with map
const prices = [9.99, 24.50, 3.75];
console.log(prices.map(addDollar));
// ['$9.99', '$24.5', '$3.75']
Partial Application
Partial application is related to currying but is more flexible — you fix some arguments and leave the rest open.
// Manual partial application
function partial(fn, ...fixedArgs) {
return function (...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
const sayHi = partial(greet, 'Hi');
console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob')); // Hi, Bob!
// Using bind for partial application
const sayHey = greet.bind(null, 'Hey');
console.log(sayHey('Charlie')); // Hey, Charlie!
Generic Curry Utility
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return function (...moreArgs) {
return curried(...args, ...moreArgs);
};
};
}
// Usage
const add = curry((a, b, c) => a + b + c);
// All these work
add(1, 2, 3); // 6
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6
// Create specialized versions
const add10 = add(10);
const add10and20 = add(10, 20);
console.log(add10(5, 3)); // 18
console.log(add10and20(30)); // 60
Currying vs Partial Application
Technique What It Does Example
─────────────────────────────────────────────────────────────────────────────
Currying Converts f(a, b, c) into f(a)(b)(c)
f(a)(b)(c)
Partial Application Fixes some arguments, f(a, b) -> g(c)
returns function for rest where g(c) = f(a, b, c)
Practical Patterns
Debounce
Debounce delays execution until a pause in calls. Useful for search inputs, window resize, etc.
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage: search input
const searchInput = document.querySelector('#search');
const search = debounce((query) => {
console.log(`Searching for: ${query}`);
// fetch(`/api/search?q=${query}`)...
}, 300);
searchInput.addEventListener('input', (e) => {
search(e.target.value);
});
How debounce works:
User types: H e l l o
| | | | |
v v v v v
Debounce: [--X--X--X--X--wait 300ms--] -> search("Hello")
Without debounce: 5 API calls (H, He, Hel, Hell, Hello)
With debounce: 1 API call (Hello)
Throttle
Throttle ensures a function runs at most once per time interval. Useful for scroll handlers, mouse move, etc.
function throttle(fn, interval) {
let lastCallTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastCallTime >= interval) {
lastCallTime = now;
fn.apply(this, args);
}
};
}
// Usage: scroll handler
const onScroll = throttle(() => {
const scrollY = window.scrollY;
console.log(`Scroll position: ${scrollY}`);
updateProgressBar(scrollY);
}, 100);
window.addEventListener('scroll', onScroll);
How throttle works:
Events: * * * * * * * * * * * * (rapid scroll events)
| |
Throttle: [execute]---100ms---[execute]---100ms---[execute]
Without throttle: 12 handler calls
With throttle: 3 handler calls (one per 100ms)
Debounce vs Throttle
Pattern When It Fires Best For
──────────────────────────────────────────────────────────────
Debounce After activity stops Search input, form validation,
(waits for pause) window resize end
Throttle At regular intervals Scroll handlers, mouse move,
during activity game loops, rate limiting
Memoize
Cache function results based on arguments. Covered in depth in the closures article, but here is the HOF perspective:
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Fibonacci — exponential time without memoization
const fib = memoize(function (n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(50)); // 12586269025 (instant with memoization)
once
Ensure a function can only be called one time.
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return { ready: true };
});
initialize(); // Initializing... -> { ready: true }
initialize(); // { ready: true } (no log, returns cached result)
initialize(); // { ready: true }
after
Create a function that only runs after being called n times.
function after(n, fn) {
let count = 0;
return function (...args) {
count++;
if (count >= n) {
return fn.apply(this, args);
}
};
}
// Run callback only after all 3 API calls complete
const allDone = after(3, () => {
console.log('All data loaded');
renderDashboard();
});
fetchUsers().then(allDone);
fetchOrders().then(allDone);
fetchProducts().then(allDone);
// "All data loaded" prints only after all three resolve
Callback Hell and Solutions
When callbacks depend on each other, you get deeply nested code known as "callback hell" or the "pyramid of doom."
// Callback hell
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
getShippingInfo(details.shippingId, (shipping) => {
displayDashboard(user, orders, details, shipping);
});
});
});
});
Solution 1: Named Functions
function displayUserDashboard(userId) {
getUser(userId, handleUser);
}
function handleUser(user) {
getOrders(user.id, (orders) => handleOrders(user, orders));
}
function handleOrders(user, orders) {
getOrderDetails(orders[0].id, (details) =>
handleDetails(user, orders, details)
);
}
function handleDetails(user, orders, details) {
getShippingInfo(details.shippingId, (shipping) =>
displayDashboard(user, orders, details, shipping)
);
}
Solution 2: Promises (Modern Standard)
getUser(userId)
.then((user) => getOrders(user.id).then((orders) => ({ user, orders })))
.then(({ user, orders }) =>
getOrderDetails(orders[0].id).then((details) => ({ user, orders, details }))
)
.then(({ user, orders, details }) =>
getShippingInfo(details.shippingId).then((shipping) =>
displayDashboard(user, orders, details, shipping)
)
);
Solution 3: async/await (Best)
async function displayUserDashboard(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const shipping = await getShippingInfo(details.shippingId);
displayDashboard(user, orders, details, shipping);
}
Higher-Order Functions at a Glance
HOF Takes fn? Returns fn? Purpose
──────────────────────────────────────────────────────────────
Array.map yes no transform elements
Array.filter yes no select elements
Array.reduce yes no combine elements
Array.sort yes no order elements
Array.forEach yes no side effects per element
setTimeout yes no delayed execution
addEventListener yes no event handling
compose/pipe yes yes chain transformations
curry yes yes partial argument binding
debounce yes yes delay until pause
throttle yes yes limit call frequency
memoize yes yes cache results
once yes yes single execution
Key Takeaways
- Functions are first-class values in JavaScript — they can be passed, returned, and stored like any other value
- A callback is a function passed to another function, to be invoked later
- A higher-order function takes functions as arguments, returns functions, or both
- Composition (
pipe/compose) lets you build complex transformations from simple functions - Currying converts multi-argument functions into chains of single-argument functions, enabling specialization
- Partial application fixes some arguments upfront, creating a more specific function
- Debounce waits for activity to stop before executing — use for search inputs
- Throttle limits execution to at most once per interval — use for scroll and resize handlers
- Memoize caches results to avoid redundant computation
- Callback hell is solved by named functions, Promises, or async/await — prefer the latter in modern code
- Understanding HOFs unlocks the functional programming paradigm in JavaScript