Higher-Order Functions in JavaScript: Full Guide
Learn what higher-order functions are in JavaScript and how to use them effectively. Covers map, filter, reduce, forEach, sort, custom HOFs, function composition, and practical patterns for cleaner code.
A higher-order function is a function that takes another function as an argument, returns a function, or both. Higher-order functions are central to JavaScript programming - array methods like map(), filter(), and reduce() are all higher-order functions. Understanding them unlocks cleaner, more expressive, and more reusable code.
What Makes a Function "Higher-Order"?
In JavaScript, functions are first-class values. You can store them in variables, pass them as arguments, and return them from other functions. A higher-order function takes advantage of this by either:
- Accepting a function as an argument (like
map,filter,addEventListener) - Returning a function (like factory functions and closures)
// Higher-order: accepts a function as argument
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Iteration ${i}`));
// Iteration 0
// Iteration 1
// Iteration 2
// Higher-order: returns a function
function createGreeter(greeting) {
return function (name) {
return `${greeting}, ${name}!`;
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob")); // "Hi, Bob!"First-Order vs Higher-Order
| Type | Definition | Example |
|---|---|---|
| First-order function | Takes only data as arguments, returns data | Math.max(1, 2, 3) |
| Higher-order function | Takes or returns a function | [1,2,3].map(fn) |
Built-In Higher-Order Functions
Array.prototype.map()
map() creates a new array by applying a function to every element:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
];
const names = users.map((user) => user.name);
console.log(names); // ["Alice", "Bob"]
// map with index
const indexed = ["a", "b", "c"].map((letter, i) => `${i}: ${letter}`);
console.log(indexed); // ["0: a", "1: b", "2: c"]Array.prototype.filter()
filter() creates a new array with elements that pass a test:
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
const users = [
{ name: "Alice", active: true },
{ name: "Bob", active: false },
{ name: "Charlie", active: true },
];
const activeUsers = users.filter((user) => user.active);
console.log(activeUsers.length); // 2Array.prototype.reduce()
reduce() processes all elements and accumulates them into a single value:
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 15
// Count occurrences
const fruits = ["apple", "banana", "apple", "cherry", "banana", "apple"];
const counts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, cherry: 1 }
// Group by property
const people = [
{ name: "Alice", department: "Engineering" },
{ name: "Bob", department: "Marketing" },
{ name: "Charlie", department: "Engineering" },
];
const byDepartment = people.reduce((groups, person) => {
const dept = person.department;
groups[dept] = groups[dept] || [];
groups[dept].push(person);
return groups;
}, {});Array.prototype.forEach()
forEach() executes a function for each element but does not return a new array:
const items = ["apple", "banana", "cherry"];
items.forEach((item, index) => {
console.log(`${index + 1}. ${item}`);
});
// 1. apple
// 2. banana
// 3. cherryforEach vs map
Use map() when you need a new array of transformed values. Use forEach() when you are performing side effects (logging, DOM updates) and do not need a return value. map() returns a new array; forEach() returns undefined.
Array.prototype.sort()
sort() accepts a comparison function:
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
// Ascending
const ascending = [...numbers].sort((a, b) => a - b);
console.log(ascending); // [1, 1, 2, 3, 4, 5, 6, 9]
// Descending
const descending = [...numbers].sort((a, b) => b - a);
console.log(descending); // [9, 6, 5, 4, 3, 2, 1, 1]
// Sort objects by property
const users = [
{ name: "Charlie", age: 30 },
{ name: "Alice", age: 25 },
{ name: "Bob", age: 35 },
];
const byAge = [...users].sort((a, b) => a.age - b.age);
const byName = [...users].sort((a, b) => a.name.localeCompare(b.name));Other Built-In HOFs
const numbers = [1, 2, 3, 4, 5];
// find: returns first matching element
const firstEven = numbers.find((n) => n % 2 === 0); // 2
// findIndex: returns index of first match
const firstEvenIndex = numbers.findIndex((n) => n % 2 === 0); // 1
// some: returns true if any element passes
const hasNegative = numbers.some((n) => n < 0); // false
// every: returns true if all elements pass
const allPositive = numbers.every((n) => n > 0); // true
// flatMap: maps then flattens one level
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap((s) => s.split(" "));
// ["hello", "world", "foo", "bar"]Chaining Higher-Order Functions
Chain multiple HOFs to build data transformation pipelines:
const transactions = [
{ id: 1, amount: 250, type: "sale", date: "2024-01-15" },
{ id: 2, amount: -50, type: "refund", date: "2024-01-16" },
{ id: 3, amount: 120, type: "sale", date: "2024-02-01" },
{ id: 4, amount: 800, type: "sale", date: "2024-01-20" },
{ id: 5, amount: -30, type: "refund", date: "2024-02-05" },
];
const janSalesTotal = transactions
.filter((t) => t.type === "sale") // only sales
.filter((t) => t.date.startsWith("2024-01")) // only January
.map((t) => t.amount) // extract amounts
.reduce((sum, amount) => sum + amount, 0); // sum them
console.log(janSalesTotal); // 1050Processing User Data Pipeline
const rawUsers = [
{ name: " alice ", email: "ALICE@EXAMPLE.COM", active: true, age: 25 },
{ name: " bob ", email: "BOB@TEST.COM", active: false, age: 17 },
{ name: "charlie", email: "CHARLIE@EXAMPLE.COM", active: true, age: 30 },
{ name: " diana ", email: "DIANA@EXAMPLE.COM", active: true, age: 22 },
];
const processedUsers = rawUsers
.filter((u) => u.active && u.age >= 18)
.map((u) => ({
name: u.name.trim().replace(/^\w/, (c) => c.toUpperCase()),
email: u.email.toLowerCase(),
ageGroup: u.age < 25 ? "young" : "adult",
}))
.sort((a, b) => a.name.localeCompare(b.name));
console.log(processedUsers);
// [
// { name: "Alice", email: "alice@example.com", ageGroup: "adult" },
// { name: "Charlie", email: "charlie@example.com", ageGroup: "adult" },
// { name: "Diana", email: "diana@example.com", ageGroup: "young" }
// ]Writing Custom Higher-Order Functions
Function That Accepts a Function
// Custom retry logic
function retry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`Attempt ${attempt} failed, retrying...`);
}
}
}
// Usage
const result = retry(() => {
// some operation that might fail
return JSON.parse('{"valid": true}');
}, 3);Function That Returns a Function
// Create a validator factory
function createValidator(rules) {
return function (value) {
const errors = [];
for (const rule of rules) {
const error = rule(value);
if (error) errors.push(error);
}
return { isValid: errors.length === 0, errors };
};
}
// Define rules as [callback functions](/tutorials/programming-languages/javascript/what-is-a-callback-function-in-js-full-tutorial)
const required = (v) => (!v ? "Field is required" : null);
const minLength = (min) => (v) => v.length < min ? `Minimum ${min} characters` : null;
const isEmail = (v) => !v.includes("@") ? "Invalid email" : null;
// Create validators
const validateEmail = createValidator([required, isEmail]);
const validatePassword = createValidator([required, minLength(8)]);
console.log(validateEmail("user@example.com")); // { isValid: true, errors: [] }
console.log(validateEmail("")); // { isValid: false, errors: ["Field is required", "Invalid email"] }
console.log(validatePassword("short")); // { isValid: false, errors: ["Minimum 8 characters"] }Timing Decorator
function withTiming(fn, label = fn.name) {
return function (...args) {
const start = performance.now();
const result = fn(...args);
const duration = performance.now() - start;
console.log(`${label} took ${duration.toFixed(2)}ms`);
return result;
};
}
function expensiveCalculation(n) {
let sum = 0;
for (let i = 0; i < n; i++) sum += i;
return sum;
}
const timedCalc = withTiming(expensiveCalculation);
timedCalc(10000000); // "expensiveCalculation took 12.34ms"Once (Run Only Once)
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(); // (no log) -> { ready: true } (cached)
initialize(); // (no log) -> { ready: true } (cached)Debounce
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Only fires after the user stops typing for 300ms
const handleSearch = debounce((query) => {
console.log(`Searching for: ${query}`);
}, 300);
// Simulating rapid input
handleSearch("h");
handleSearch("he");
handleSearch("hel");
handleSearch("hello"); // Only this one fires (after 300ms)Function Composition
Combine pure functions into pipelines:
// Compose: right to left
function compose(...fns) {
return (x) => fns.reduceRight((value, fn) => fn(value), x);
}
// Pipe: left to right
function pipe(...fns) {
return (x) => fns.reduce((value, fn) => fn(value), x);
}
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const replaceSpaces = (str) => str.replace(/\s+/g, "-");
const truncate = (str) => str.slice(0, 30);
// Create a slug generator by composing functions
const slugify = pipe(trim, toLowerCase, replaceSpaces, truncate);
console.log(slugify(" Hello World ")); // "hello-world"
console.log(slugify(" JavaScript IIFE ")); // "javascript-iife"Composing With Multiple Arguments
// Partial application for multi-argument functions
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function multiply(a, b) {
return a * b;
}
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log([1, 2, 3, 4].map(double)); // [2, 4, 6, 8]Practical Patterns
Event Handler Factory
function createClickHandler(action, data) {
return function (event) {
event.preventDefault();
console.log(`Action: ${action}`, data);
};
}
// Each button gets its own handler with captured data
document.getElementById("save").addEventListener(
"click",
createClickHandler("save", { formId: "userForm" })
);
document.getElementById("delete").addEventListener(
"click",
createClickHandler("delete", { itemId: 42 })
);Permission Checker
function requiresRole(role) {
return function (handler) {
return function (request) {
if (request.user.role !== role) {
return { status: 403, message: "Forbidden" };
}
return handler(request);
};
};
}
const adminOnly = requiresRole("admin");
const deleteUser = adminOnly((request) => {
return { status: 200, message: `Deleted user ${request.params.id}` };
});
// Test
console.log(deleteUser({ user: { role: "user" }, params: { id: 1 } }));
// { status: 403, message: "Forbidden" }
console.log(deleteUser({ user: { role: "admin" }, params: { id: 1 } }));
// { status: 200, message: "Deleted user 1" }Array Utility Kit
// Higher-order utility functions for arrays
const pluck = (key) => (arr) => arr.map((item) => item[key]);
const where = (predicate) => (arr) => arr.filter(predicate);
const sortBy = (key) => (arr) =>
[...arr].sort((a, b) => (a[key] > b[key] ? 1 : -1));
const groupBy = (key) => (arr) =>
arr.reduce((groups, item) => {
const group = item[key];
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {});
const users = [
{ name: "Charlie", department: "Engineering", age: 30 },
{ name: "Alice", department: "Marketing", age: 25 },
{ name: "Bob", department: "Engineering", age: 35 },
];
const getNames = pluck("name");
const getActive = where((u) => u.age > 25);
const sortByName = sortBy("name");
const groupByDept = groupBy("department");
console.log(getNames(users)); // ["Charlie", "Alice", "Bob"]
console.log(getActive(users)); // [Charlie, Bob]
console.log(sortByName(users)); // [Alice, Bob, Charlie]
console.log(groupByDept(users)); // { Engineering: [...], Marketing: [...] }Common Mistakes
1. Forgetting to Return in map
// BUG: arrow function with braces needs explicit return
const doubled = [1, 2, 3].map((n) => {
n * 2; // missing return!
});
console.log(doubled); // [undefined, undefined, undefined]
// FIX: add return or use concise body
const doubled = [1, 2, 3].map((n) => n * 2);2. Using map for Side Effects
// WRONG: use forEach for side effects
[1, 2, 3].map((n) => console.log(n)); // creates unused array
// RIGHT: forEach for side effects
[1, 2, 3].forEach((n) => console.log(n));3. Mutating in reduce
// Risky: mutating the accumulator works but is confusing
const grouped = items.reduce((acc, item) => {
acc[item.type] = acc[item.type] || [];
acc[item.type].push(item); // mutating acc
return acc;
}, {});
// This is a common pattern and acceptable, but be aware
// that the accumulator object is being mutated each iterationRune AI
Key Insights
- Higher-order = takes or returns functions:
map,filter,reduceare classic examples - Chain for transformations:
.filter().map().reduce()builds clean data pipelines - Create factories: return functions with captured state for reusable logic
- Compose small functions:
pipe(fn1, fn2, fn3)for complex transformations - Use the right method:
mapfor transformation,filterfor selection,reducefor accumulation - Arrow functions for brevity:
(x) => x * 2is ideal for inline callbacks
Frequently Asked Questions
What is the difference between map and forEach?
When should I use reduce vs a [for loop](/tutorials/programming-languages/javascript/js-for-loop-syntax-a-complete-guide-for-beginners)?
Are arrow functions always better for HOFs?
Can higher-order functions be async?
Conclusion
Higher-order functions take functions as arguments, return functions, or both. JavaScript's built-in array methods (map, filter, reduce, sort, find, some, every) are all higher-order functions. Custom HOFs like factories, decorators, and validators make your code reusable and composable. Chain array HOFs to build data transformation pipelines. Use function composition (pipe/compose) to build complex operations from simple, pure functions.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.