docs/concepts/higher-order-functions.mdx
What if you could tell a function how to do something, not just what data to work with? What if you could pass behavior itself as an argument, just like you pass numbers or strings?
// Without higher-order functions: repetitive code
for (let i = 0; i < 3; i++) {
console.log(i)
}
// With higher-order functions: reusable abstraction
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i)
}
}
repeat(3, console.log) // 0, 1, 2
repeat(3, i => console.log(i * 2)) // 0, 2, 4
This is the power of higher-order functions. They let you write functions that are flexible, reusable, and abstract. Instead of writing the same loop over and over with slightly different logic, you write one function and pass in the logic that changes. As MDN documents, JavaScript treats functions as first-class citizens — they can be assigned to variables, passed as arguments, and returned from other functions — which is the foundation that makes higher-order functions possible.
<Info> **What you'll learn in this guide:** - What makes a function "higher-order" - The connection between first-class functions and HOFs - How to create functions that accept other functions - How to create functions that return other functions (function factories) - How closures enable higher-order functions - Common mistakes and how to avoid them - When and why to use higher-order functions </Info> <Warning> **Prerequisites:** This guide assumes you understand [scope and closures](/concepts/scope-and-closures). Closures are created when higher-order functions return other functions. You should also be familiar with [callbacks](/concepts/callbacks), since callbacks are the functions being passed to higher-order functions. </Warning>A higher-order function is a function that does at least one of these two things:
That's it. If a function takes a function or returns a function, it's higher-order. The ECMAScript specification defines functions as callable objects, and because JavaScript allows any object to be passed around, functions naturally flow through higher-order patterns. According to the State of JS 2023 survey, functional programming techniques like higher-order functions rank among the most widely used JavaScript patterns.
// 1. Accepts a function as an argument
function doTwice(action) {
action()
action()
}
doTwice(() => console.log('Hello!'))
// Hello!
// Hello!
// 2. Returns a function as its result
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`
}
}
const sayHello = createGreeter('Hello')
console.log(sayHello('Alice')) // Hello, Alice!
console.log(sayHello('Bob')) // Hello, Bob!
Higher-order functions let you:
Without higher-order functions, you'd repeat the same patterns over and over. With them, you write flexible code that adapts to different needs.
To understand why higher-order functions matter, let's look at an analogy from Eloquent JavaScript.
Compare these two recipes for pea soup:
Recipe 1 (Low-level instructions):
Put 1 cup of dried peas per person into a container. Add water until the peas are well covered. Leave the peas in water for at least 12 hours. Take the peas out of the water and put them in a cooking pan. Add 4 cups of water per person. Cover the pan and keep the peas simmering for two hours. Take half an onion per person. Cut it into pieces with a knife. Add it to the peas...
Recipe 2 (Higher-level instructions):
Per person: 1 cup dried split peas, 4 cups of water, half a chopped onion, a stalk of celery, and a carrot.
Soak peas for 12 hours. Simmer for 2 hours. Chop and add vegetables. Cook for 10 more minutes.
The second recipe is shorter and easier to understand. But it requires you to know what "soak", "simmer", and "chop" mean. These are abstractions. They hide the step-by-step details behind meaningful names.
┌─────────────────────────────────────────────────────────────────────────┐
│ LEVELS OF ABSTRACTION │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ HIGH LEVEL (What you want) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ "Calculate the area for each radius" │ │
│ │ │ │
│ │ radii.map(calculateArea) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ MEDIUM LEVEL (How to iterate) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ function map(array, transform) { │ │
│ │ const result = [] │ │
│ │ for (const item of array) { │ │
│ │ result.push(transform(item)) │ │
│ │ } │ │
│ │ return result │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ LOW LEVEL (Step by step) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ const result = [] │ │
│ │ for (let i = 0; i < radii.length; i++) { │ │
│ │ const radius = radii[i] │ │
│ │ const area = Math.PI * radius * radius │ │
│ │ result.push(area) │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ Higher-order functions let you work at the level that makes sense │
│ for your problem, hiding the mechanical details below. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Higher-order functions are how we create these abstractions in JavaScript. We package up common patterns (like "do something to each item") into reusable functions, then pass in the specific behavior we need.
Higher-order functions are possible because JavaScript has first-class functions. This means functions are treated like any other value. You can:
// Functions are values, just like numbers or strings
const greet = function(name) {
return `Hello, ${name}!`
}
// Arrow functions work the same way
const add = (a, b) => a + b
console.log(greet('Alice')) // Hello, Alice!
console.log(add(2, 3)) // 5
function callTwice(fn) {
fn()
fn()
}
callTwice(function() {
console.log('This runs twice!')
})
// This runs twice!
// This runs twice!
function createMultiplier(multiplier) {
// This returned function "remembers" the multiplier
return function(number) {
return number * multiplier
}
}
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
The most common type of HOF accepts a function as an argument. You pass in what should happen, and the HOF handles when and how it happens.
repeat FunctionInstead of writing loops everywhere, create a function that handles the looping:
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i)
}
}
// Now you can reuse this for any repeated action
repeat(3, i => console.log(`Iteration ${i}`))
// Iteration 0
// Iteration 1
// Iteration 2
repeat(5, i => console.log('*'.repeat(i + 1)))
// *
// **
// ***
// ****
// *****
The repeat function doesn't know or care what action you want to perform. It just knows how to repeat something. You provide the "something."
calculate FunctionSuppose you need to calculate different properties of circles:
// Without HOF: repetitive code
function calculateAreas(radii) {
const result = []
for (let i = 0; i < radii.length; i++) {
result.push(Math.PI * radii[i] * radii[i])
}
return result
}
function calculateCircumferences(radii) {
const result = []
for (let i = 0; i < radii.length; i++) {
result.push(2 * Math.PI * radii[i])
}
return result
}
function calculateDiameters(radii) {
const result = []
for (let i = 0; i < radii.length; i++) {
result.push(2 * radii[i])
}
return result
}
That's a lot of repetition! The only thing that changes is the formula. Let's use a higher-order function:
// With HOF: write the loop once, pass in the logic
function calculate(radii, formula) {
const result = []
for (const radius of radii) {
result.push(formula(radius))
}
return result
}
// Define the specific logic separately
const area = r => Math.PI * r * r
const circumference = r => 2 * Math.PI * r
const diameter = r => 2 * r
const radii = [1, 2, 3]
console.log(calculate(radii, area))
// [3.14159..., 12.56637..., 28.27433...]
console.log(calculate(radii, circumference))
// [6.28318..., 12.56637..., 18.84955...]
console.log(calculate(radii, diameter))
// [2, 4, 6]
Now adding a new calculation is easy. Just write a new formula function:
// Works for any formula that takes a radius!
const squaredRadius = r => r * r
console.log(calculate(radii, squaredRadius)) // [1, 4, 9]
unless FunctionYou can create new control flow abstractions:
function unless(condition, action) {
if (!condition) {
action()
}
}
// Use it to express "do this unless that"
repeat(5, n => {
unless(n % 2 === 1, () => {
console.log(n, 'is even')
})
})
// 0 is even
// 2 is even
// 4 is even
This reads almost like English: "Unless n is odd, log that it's even."
The second type of HOF returns a function. This is powerful because the returned function can "remember" values from when it was created.
greaterThan Factoryfunction greaterThan(n) {
return function(m) {
return m > n
}
}
const greaterThan10 = greaterThan(10)
const greaterThan100 = greaterThan(100)
console.log(greaterThan10(11)) // true
console.log(greaterThan10(5)) // false
console.log(greaterThan100(50)) // false
console.log(greaterThan100(150)) // true
greaterThan is a function factory. You give it a number, and it manufactures a new function that tests if other numbers are greater than that number.
multiplier Factoryfunction multiplier(factor) {
return number => number * factor
}
const double = multiplier(2)
const triple = multiplier(3)
const tenX = multiplier(10)
console.log(double(5)) // 10
console.log(triple(5)) // 15
console.log(tenX(5)) // 50
// You can use the factory directly too
console.log(multiplier(7)(3)) // 21
noisy WrapperHigher-order functions can wrap other functions to add behavior:
function noisy(fn) {
return function(...args) {
console.log('Calling with arguments:', args)
const result = fn(...args)
console.log('Returned:', result)
return result
}
}
const noisyMax = noisy(Math.max)
noisyMax(3, 1, 4, 1, 5)
// Calling with arguments: [3, 1, 4, 1, 5]
// Returned: 5
const noisyFloor = noisy(Math.floor)
noisyFloor(4.7)
// Calling with arguments: [4.7]
// Returned: 4
The original functions (Math.max, Math.floor) are unchanged. We've created new functions that log their inputs and outputs, wrapping the original behavior.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE WRAPPER PATTERN │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Original Function Wrapped Function │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ 1. Log the arguments │ │
│ │ Math.max │ noisy() │ 2. Call Math.max │ │
│ │ │ ────────► │ 3. Log the result │ │
│ │ (3,1,4,1,5) → 5 │ │ 4. Return the result │ │
│ │ │ │ │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
│ │
│ The wrapper adds behavior before and after, without changing │
│ the original function. This is the "decorator" pattern. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Function factories are functions that create and return other functions. They're useful when you need many similar functions that differ only in some configuration.
function createValidator(min, max) {
return function(value) {
return value >= min && value <= max
}
}
const isValidAge = createValidator(0, 120)
const isValidPercentage = createValidator(0, 100)
const isValidRating = createValidator(1, 5)
console.log(isValidAge(25)) // true
console.log(isValidAge(150)) // false
console.log(isValidPercentage(50)) // true
console.log(isValidPercentage(101)) // false
console.log(isValidRating(3)) // true
function createFormatter(prefix, suffix) {
return function(value) {
return `${prefix}${value}${suffix}`
}
}
const formatDollars = createFormatter('$', '')
const formatPercent = createFormatter('', '%')
const formatParens = createFormatter('(', ')')
console.log(formatDollars(99.99)) // $99.99
console.log(formatPercent(75)) // 75%
console.log(formatParens('aside')) // (aside)
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs)
}
}
function greet(greeting, punctuation, name) {
return `${greeting}, ${name}${punctuation}`
}
const sayHello = partial(greet, 'Hello', '!')
const askHowAreYou = partial(greet, 'How are you', '?')
console.log(sayHello('Alice')) // Hello, Alice!
console.log(sayHello('Bob')) // Hello, Bob!
console.log(askHowAreYou('Charlie')) // How are you, Charlie?
Higher-order functions that return functions rely on closures. When a function is created inside another function, it "closes over" the variables in its surrounding scope, remembering them even after the outer function has finished.
function createCounter(start = 0) {
let count = start // This variable is "enclosed"
return function() {
count++ // The inner function can access and modify it
return count
}
}
const counter1 = createCounter()
const counter2 = createCounter(100)
console.log(counter1()) // 1
console.log(counter1()) // 2
console.log(counter1()) // 3
console.log(counter2()) // 101
console.log(counter2()) // 102
// Each counter has its own private count variable
console.log(counter1()) // 4 (not affected by counter2)
┌─────────────────────────────────────────────────────────────────────────┐
│ HOW CLOSURES WORK WITH HOFs │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ createCounter(0) createCounter(100) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ count = 0 │ │ count = 100 │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ function() { │ │ │ │ function() { │ │ │
│ │ │ count++ │◄─┼───────┐ │ │ count++ │◄─┼───────┐ │
│ │ │ return count│ │ │ │ │ return count│ │ │ │
│ │ │ } │ │ │ │ │ } │ │ │ │
│ │ └───────────────┘ │ │ │ └───────────────┘ │ │ │
│ └─────────────────────┘ │ └─────────────────────┘ │ │
│ │ │ │ │ │
│ ▼ │ ▼ │ │
│ counter1 ───────────────┘ counter2 ───────────────┘ │
│ │
│ Each returned function has its own "backpack" containing the │
│ variables from when it was created. This is a closure. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
This pattern creates truly private variables. Nothing outside can access count directly:
function createBankAccount(initialBalance) {
let balance = initialBalance // Private variable
return {
deposit(amount) {
if (amount > 0) {
balance += amount
return balance
}
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount
return balance
}
return 'Insufficient funds'
},
getBalance() {
return balance
}
}
}
const account = createBankAccount(100)
console.log(account.getBalance()) // 100
console.log(account.deposit(50)) // 150
console.log(account.withdraw(30)) // 120
// Can't access balance directly
console.log(account.balance) // undefined
JavaScript provides many built-in higher-order functions, especially for working with arrays. These are covered in depth in the Map, Reduce, and Filter guide, but here's a quick overview:
| Method | What it does | Returns |
|---|---|---|
forEach(fn) | Calls fn on each element | undefined |
map(fn) | Transforms each element with fn | New array |
filter(fn) | Keeps elements where fn returns true | New array |
reduce(fn, init) | Accumulates elements into single value | Single value |
find(fn) | Returns first element where fn returns true | Element or undefined |
some(fn) | Tests if any element passes fn | boolean |
every(fn) | Tests if all elements pass fn | boolean |
sort(fn) | Sorts elements using comparator fn | Sorted array (mutates!) |
const numbers = [1, 2, 3, 4, 5]
// All of these accept a function as an argument
numbers.forEach(n => console.log(n)) // Logs each number
numbers.map(n => n * 2) // [2, 4, 6, 8, 10]
numbers.filter(n => n > 2) // [3, 4, 5]
numbers.reduce((sum, n) => sum + n, 0) // 15
numbers.find(n => n > 3) // 4
numbers.some(n => n > 4) // true
numbers.every(n => n > 0) // true
When using curly braces in arrow functions, you must explicitly return:
// ❌ WRONG - implicit return only works without braces
const double = numbers.map(n => {
n * 2 // This doesn't return anything!
})
console.log(double) // [undefined, undefined, undefined, ...]
// ✓ CORRECT - explicit return with braces
const double = numbers.map(n => {
return n * 2
})
// ✓ CORRECT - implicit return without braces
const double = numbers.map(n => n * 2)
this ContextWhen passing methods as callbacks, this may not be what you expect:
const user = {
name: 'Alice',
greet() {
console.log(`Hello, I'm ${this.name}`)
}
}
// ❌ WRONG - 'this' is lost
setTimeout(user.greet, 1000) // "Hello, I'm undefined"
// ✓ CORRECT - bind the context
setTimeout(user.greet.bind(user), 1000) // "Hello, I'm Alice"
// ✓ CORRECT - use an arrow function wrapper
setTimeout(() => user.greet(), 1000) // "Hello, I'm Alice"
parseInt Gotcha with mapmap passes three arguments to its callback: (element, index, array). Some functions don't expect this:
// ❌ WRONG - parseInt receives (string, index) and uses index as radix
['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
// Why? map calls:
// parseInt('1', 0) → 1 (radix 0 is treated as 10)
// parseInt('2', 1) → NaN (radix 1 is invalid)
// parseInt('3', 2) → NaN (3 is not valid in binary)
// ✓ CORRECT - wrap parseInt to only pass the string
['1', '2', '3'].map(str => parseInt(str, 10)) // [1, 2, 3]
// ✓ CORRECT - use Number instead
['1', '2', '3'].map(Number) // [1, 2, 3]
Don't force HOFs when a simple loop would be clearer:
// Sometimes this is clearer...
let sum = 0
for (const n of numbers) {
sum += n
}
// ...than this (for simple cases)
const sum = numbers.reduce((acc, n) => acc + n, 0)
Use HOFs when they make the code more readable, not just to seem clever.
A higher-order function accepts functions as arguments OR returns a function. If it does either, it's higher-order.
First-class functions make HOFs possible. In JavaScript, functions are values you can assign, pass, and return.
HOFs that accept functions let you parameterize behavior. Write the structure once, pass in what varies.
HOFs that return functions create function factories. They "manufacture" specialized functions from a template.
Closures are the key to functions returning functions. The returned function remembers variables from when it was created.
Built-in array methods like map, filter, reduce, forEach, find, some, and every are all higher-order functions.
The abstraction benefit is huge. HOFs let you work at the right level of abstraction, hiding mechanical details.
Watch out for common gotchas like losing this, forgetting to return, and unexpected arguments like with parseInt.
Don't overuse HOFs. Sometimes a simple loop is clearer. Use HOFs when they make code more readable, not less.
</Info>A function is higher-order if it does at least one of these two things:
1. Accepts one or more functions as arguments
2. Returns a function as its result
```javascript
// Accepts a function
function doTwice(fn) {
fn()
fn()
}
// Returns a function
function multiplier(factor) {
return n => n * factor
}
// Does both!
function compose(f, g) {
return x => f(g(x))
}
```
They're two sides of the same coin:
- A **callback** is a function passed to another function to be executed later
- A **higher-order function** is a function that accepts (or returns) other functions
When you pass a callback to a higher-order function, the HOF decides when to call it.
```javascript
// setTimeout is a higher-order function
// The arrow function is the callback
setTimeout(() => console.log('Done!'), 1000)
// map is a higher-order function
// n => n * 2 is the callback
[1, 2, 3].map(n => n * 2)
```
`map` passes three arguments to its callback: `(element, index, array)`.
`parseInt` accepts two arguments: `(string, radix)`. So `map` accidentally passes the index as the radix:
```javascript
// What map actually calls:
parseInt('1', 0) // 1 (radix 0 → default base 10)
parseInt('2', 1) // NaN (radix 1 is invalid)
parseInt('3', 2) // NaN (3 is not valid binary)
```
The fix is to wrap `parseInt`:
```javascript
['1', '2', '3'].map(str => parseInt(str, 10)) // [1, 2, 3]
// or
['1', '2', '3'].map(Number) // [1, 2, 3]
```
When a function returns another function, the inner function "closes over" variables from the outer function's scope. It remembers them even after the outer function has finished.
```javascript
function createMultiplier(factor) {
// 'factor' is captured by the returned function
return function(number) {
return number * factor
}
}
const double = createMultiplier(2) // factor = 2 is remembered
const triple = createMultiplier(3) // factor = 3 is remembered
console.log(double(5)) // 10 (uses factor = 2)
console.log(triple(5)) // 15 (uses factor = 3)
```
Each returned function has its own closure with its own `factor` value.
Avoid HOFs when:
1. **A simple loop is clearer** for your specific case
2. **Performance is critical** (loops can be faster for simple operations)
3. **The abstraction adds more complexity** than it removes
4. **You're chaining too many operations** making debugging hard
```javascript
// Sometimes this is perfectly fine:
let sum = 0
for (const n of numbers) {
sum += n
}
// Don't force this just to use HOFs:
const sum = numbers.reduce((acc, n) => acc + n, 0)
```
The goal is readable, maintainable code. Use whatever achieves that.
| Aspect | `map()` | `forEach()` |
|--------|---------|-------------|
| Returns | New array with transformed elements | `undefined` |
| Purpose | Transform data | Perform side effects |
| Chainable | Yes | No |
| Use when | You need the result | You just want to do something |
```javascript
const numbers = [1, 2, 3]
// map: transforms and returns new array
const doubled = numbers.map(n => n * 2)
console.log(doubled) // [2, 4, 6]
// forEach: just executes, returns undefined
const result = numbers.forEach(n => console.log(n))
console.log(result) // undefined
```
Use `map` when you need the transformed array. Use `forEach` when you just want to do something with each element (like logging or updating external state).