Higher-order functions are the foundation of JavaScript's functional style. Master map, filter, reduce, and how to implement your own.
Picture a power tool versus a hand tool. A hammer does one thing — it hits nails. A drill press is different: you load it with different drill bits, and it becomes a different tool depending on what you give it. You can swap bits to drill wood, metal, or plastic — same machine, completely different behavior. Higher-order functions are the drill press. Instead of being hardwired to do one thing, they accept other functions as inputs (the drill bits) and their behavior changes based on what you give them. Or they produce new specialized functions as their output, the way a drill press could theoretically stamp out custom drill bits. The key insight: when functions can be passed around like any other value — stored in variables, passed as arguments, returned from other functions — you unlock the ability to separate "what to do with each item" from "how to iterate over items." That separation is the entire foundation of clean, reusable JavaScript.
A higher-order function (HOF) is any function that either:
This is possible because JavaScript treats functions as first-class citizens — functions are just values, like numbers or strings. You can store them in variables, put them in arrays, pass them as arguments, and return them from other functions.
// Functions are values — these are all equivalent
function add(a, b) { return a + b }
const add = function(a, b) { return a + b }
const add = (a, b) => a + b
// Store in a variable
const operation = add
operation(2, 3) // 5
// Store in an array
const fns = [Math.sin, Math.cos, Math.sqrt]
fns[0](Math.PI) // ≈ 0
// Store in an object
const math = { add, multiply: (a, b) => a * b }
math.add(2, 3) // 5
// Pass as an argument — HOF
function applyTwice(fn, x) {
return fn(fn(x))
}
applyTwice(x => x + 3, 10) // 16 — (10+3)+3
applyTwice(x => x * 2, 5) // 20 — (5*2)*2
// Return a function — HOF
function makeMultiplier(n) {
return x => x * n
}
const double = makeMultiplier(2)
const triple = makeMultiplier(3)
double(5) // 10
triple(5) // 15
Creates a new array by applying a function to each element. The original array is never modified.
const numbers = [1, 2, 3, 4, 5]
// Without HOF — imperative
const doubled = []
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2)
}
// With map — declarative
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]
// Callback signature: (currentValue, index, array)
const withIndex = numbers.map((n, i) => `${i}: ${n}`)
// ['0: 1', '1: 2', '2: 3', '3: 4', '4: 5']
// Real-world: transform API data
const users = [
{ id: 1, first: 'Alice', last: 'Smith' },
{ id: 2, first: 'Bob', last: 'Jones' },
]
const names = users.map(u => `${u.first} ${u.last}`)
const ids = users.map(u => u.id)
const userMap = users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` }))
Creates a new array containing only elements for which the callback returns a truthy value.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const evens = numbers.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
const odds = numbers.filter(n => n % 2 !== 0) // [1, 3, 5, 7, 9]
const over5 = numbers.filter(n => n > 5) // [6, 7, 8, 9, 10]
// Real-world: filtering UI state
const tasks = [
{ id: 1, text: 'Buy milk', done: true },
{ id: 2, text: 'Write code', done: false },
{ id: 3, text: 'Read book', done: false },
]
const pending = tasks.filter(t => !t.done)
const completed = tasks.filter(t => t.done)
const withText = tasks.filter(t => t.text.includes('code'))
The most powerful and general of the three. It accumulates a result by running the callback on each element, threading the accumulated value through.
// Signature: reduce(callback, initialValue)
// Callback: (accumulator, currentValue, index, array)
const numbers = [1, 2, 3, 4, 5]
// Sum
const sum = numbers.reduce((acc, n) => acc + n, 0) // 15
// Product
const product = numbers.reduce((acc, n) => acc * n, 1) // 120
// Max value (without Math.max)
const max = numbers.reduce((acc, n) => n > acc ? n : acc, -Infinity) // 5
// Building an object from an array
const people = ['Alice', 'Bob', 'Carol']
const byName = people.reduce((acc, name) => {
acc[name] = { name, active: true }
return acc
}, {})
// { Alice: { name: 'Alice', active: true }, Bob: {...}, Carol: {...} }
// Grouping by a property
const orders = [
{ product: 'apple', qty: 3 },
{ product: 'banana', qty: 2 },
{ product: 'apple', qty: 5 },
]
const grouped = orders.reduce((acc, order) => {
const key = order.product
if (!acc[key]) acc[key] = []
acc[key].push(order)
return acc
}, {})
// { apple: [{qty:3},{qty:5}], banana: [{qty:2}] }
Like map but always returns undefined. Use it only for side effects (logging, DOM updates, firing events) — never to build a new array.
numbers.forEach((n, i) => console.log(`${i}: ${n}`))
// Use map if you need a result. Use forEach if you don't.
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]
const user = users.find(u => u.id === 2) // { id: 2, name: 'Bob' }
const index = users.findIndex(u => u.id === 2) // 1
const none = users.find(u => u.id === 99) // undefined — not null, not -1
const scores = [72, 88, 91, 65, 99]
scores.every(s => s >= 60) // true — all pass
scores.every(s => s >= 80) // false — not all pass
scores.some(s => s >= 90) // true — at least one passes
scores.some(s => s < 50) // false — none fail that badly
Because each HOF returns a new array, you can chain them into readable data pipelines:
const employees = [
{ name: 'Alice', dept: 'Engineering', salary: 90000, active: true },
{ name: 'Bob', dept: 'Marketing', salary: 75000, active: true },
{ name: 'Carol', dept: 'Engineering', salary: 110000, active: false },
{ name: 'Dave', dept: 'Engineering', salary: 85000, active: true },
{ name: 'Eve', dept: 'Marketing', salary: 68000, active: false },
]
// Get total salary of active Engineering employees
const totalActivEngSalary = employees
.filter(e => e.active) // active only
.filter(e => e.dept === 'Engineering') // engineering only
.map(e => e.salary) // extract salaries
.reduce((sum, s) => sum + s, 0) // total
// [Alice(90k), Dave(85k)] → [90000, 85000] → 175000
// Get sorted names of active employees
const activeNames = employees
.filter(e => e.active)
.map(e => e.name)
.sort()
// A HOF that takes a predicate and returns its opposite
const not = fn => (...args) => !fn(...args)
const isEven = n => n % 2 === 0
const isOdd = not(isEven)
[1,2,3,4,5].filter(isOdd) // [1, 3, 5]
// A HOF that memoizes another function
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
}
}
const expensiveCalc = memoize((n) => {
// simulate expensive operation
return n * n
})
expensiveCalc(42) // calculates
expensiveCalc(42) // returns cached result
// A HOF that rate-limits a function
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
const onResize = debounce(() => console.log('resized'), 200)
window.addEventListener('resize', onResize)
const orders = [
{ id: 1, amount: 150, status: 'completed' },
{ id: 2, amount: 50, status: 'pending' },
{ id: 3, amount: 200, status: 'completed' },
{ id: 4, amount: 75, status: 'pending' },
]
// Imperative — HOW to do it
let total = 0
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === 'completed') {
total += orders[i].amount
}
}
// Declarative — WHAT to get
const total = orders
.filter(o => o.status === 'completed')
.reduce((sum, o) => sum + o.amount, 0)
// Both produce 350. The declarative version reads like a sentence.Many devs think map, filter, and reduce are the only higher-order functions — but actually any function that accepts a function as an argument or returns a function is a higher-order function. setTimeout, addEventListener, Promise.then(), Array.sort(), and every custom utility that accepts a callback are all higher-order functions.
Many devs think forEach is just a cleaner for loop and can replace map — but actually forEach always returns undefined and is only for side effects. Using forEach to push into a new array is an anti-pattern. Use map when you want a transformed array, forEach only when you want to run side effects and don't need a result.
Many devs think chaining map/filter is less efficient than a single for loop because it iterates multiple times — but actually the readability benefit is almost always worth the micro-performance difference in practice. If performance is genuinely critical, use reduce in one pass — but profile first. Premature optimization of map/filter chains is a common trap.
Many devs think reduce can only sum numbers — but actually reduce is the most general array operation. It can implement map, filter, groupBy, flatten, any aggregation, or any transformation from an array to any value. If you can't figure out how to use map or filter for something, reduce can always do it.
Many devs think higher-order functions are a functional programming luxury and not for production code — but actually debounce, throttle, memoize, middleware, and event listeners are all higher-order functions used in virtually every production JavaScript codebase. React's useCallback, useMemo, and every HOC are higher-order functions.
Many devs think the original array is modified by map and filter — but actually map and filter always return new arrays, leaving the original untouched. This immutability is intentional and is why these functions are preferred over mutating methods like push, splice, and sort (which does mutate the original — a common gotcha).
React's useCallback is a higher-order function — it takes a function and a dependency array and returns a memoized version of that function. Without useCallback, passing inline arrow functions as props creates a new function reference on every render, which breaks React.memo optimizations on child components.
Redux's compose utility is a higher-order function that builds middleware pipelines — compose(f, g, h)(x) is equivalent to f(g(h(x))). Redux's applyMiddleware uses it to combine multiple middleware functions into one enhancer, and every piece of Redux middleware is itself a higher-order function (store => next => action => {}).
Lodash's debounce and throttle are the most commonly imported higher-order functions in frontend codebases — they take a function and a time limit and return a new function with rate-limiting behavior. They're used on search inputs, resize handlers, scroll listeners, and any event that fires faster than you want to process it.
Express.js is almost entirely built around higher-order functions — app.use() takes middleware functions, router.get() takes handler functions, and Express's own error handling works by detecting that a middleware function accepts 4 parameters (err, req, res, next) rather than 3.
Promise.then() is a higher-order method — it takes a callback function and returns a new Promise. The entire Promise chaining pattern is built on the HOF principle: each .then() wraps the previous result in a new computation step, which is why .then(fn1).then(fn2) pipelines work.
Array.sort() is a higher-order function that is chronically misused — without a comparator function, it converts elements to strings and sorts lexicographically, which is why [10, 9, 2, 100].sort() produces [10, 100, 2, 9]. The correct numeric sort requires the HOF: .sort((a, b) => a - b).
What is a pure function and why does it matter?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.