Back to all articles
JavaScript

Higher-Order Functions

Learn higher-order functions in JavaScript. Understand functions that accept or return other functions, create reusable abstractions, and write cleaner code.

Share this article

Send it to a friend, your team, or your favorite social feed.

Higher-Order Functions

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.

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

Prerequisites: This guide assumes you understand scope and closures. Closures are created when higher-order functions return other functions. You should also be familiar with callbacks, since callbacks are the functions being passed to higher-order functions.


What is a Higher-Order Function?

A higher-order function is a function that does at least one of these two things:

  1. Accepts one or more functions as arguments
  2. Returns a function as its result

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!

The name "higher-order" comes from mathematics, where functions that operate on other functions are considered to be at a "higher level" of abstraction. In JavaScript, we just call them higher-order functions, or HOFs for short.

Why Does This Matter?

Higher-order functions let you:

  • Avoid repetition: Write the structure once, vary the behavior
  • Create abstractions: Hide complexity behind simple interfaces
  • Build reusable utilities: Functions that work with any logic you pass them
  • Compose functionality: Combine simple functions into complex ones

Without higher-order functions, you'd repeat the same patterns over and over. With them, you write flexible code that adapts to different needs.


The Pea Soup Analogy

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.


First-Class Functions: The Foundation

Higher-order functions are possible because JavaScript has first-class functions. This means functions are treated like any other value. You can:

1. Assign Functions to Variables

// 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

2. Pass Functions as Arguments

function callTwice(fn) {
  fn()
  fn()
}

callTwice(function() {
  console.log('This runs twice!')
})
// This runs twice!
// This runs twice!

3. Return Functions from Functions

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

Not all languages have first-class functions. In C, you can pass function pointers, but the ergonomics differ from JavaScript's first-class functions: C requires explicit pointer syntax and manual management, while JavaScript treats functions as seamless values. Java added lambda expressions in version 8, but they work differently than JavaScript functions. JavaScript's first-class functions make functional programming patterns natural and powerful.


Higher-Order Functions That Accept Functions

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.

Example: A Reusable repeat Function

Instead 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."

Example: A Flexible calculate Function

Suppose 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]

Example: An unless Function

You 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."


Higher-Order Functions That Return Functions

The second type of HOF returns a function. This is powerful because the returned function can "remember" values from when it was created.

Example: The greaterThan Factory

function 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.

Example: The multiplier Factory

function 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

Example: A noisy Wrapper

Higher-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 in Practice

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.

Example: Creating Validators

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

Example: Creating Formatters

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)

Example: Pre-filling Arguments (Partial Application)

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?

The Closure Connection

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.                 │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Private Variables Through Closures

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

Built-in Higher-Order Functions

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:

MethodWhat it doesReturns
forEach(fn)Calls fn on each elementundefined
map(fn)Transforms each element with fnNew array
filter(fn)Keeps elements where fn returns trueNew array
reduce(fn, init)Accumulates elements into single valueSingle value
find(fn)Returns first element where fn returns trueElement or undefined
some(fn)Tests if any element passes fnboolean
every(fn)Tests if all elements pass fnboolean
sort(fn)Sorts elements using comparator fnSorted 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

For a deep dive into these methods with practical examples, see Map, Reduce, and Filter.


Common Mistakes

1. Forgetting to Return in Arrow Functions

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)

2. Losing this Context

When 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"

3. The parseInt Gotcha with map

map 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]

4. Using Higher-Order Functions When a Simple Loop is Clearer

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.


Key Takeaways

The key things to remember:

  1. A higher-order function accepts functions as arguments OR returns a function. If it does either, it's higher-order.

  2. First-class functions make HOFs possible. In JavaScript, functions are values you can assign, pass, and return.

  3. HOFs that accept functions let you parameterize behavior. Write the structure once, pass in what varies.

  4. HOFs that return functions create function factories. They "manufacture" specialized functions from a template.

  5. Closures are the key to functions returning functions. The returned function remembers variables from when it was created.

  6. Built-in array methods like map, filter, reduce, forEach, find, some, and every are all higher-order functions.

  7. The abstraction benefit is huge. HOFs let you work at the right level of abstraction, hiding mechanical details.

  8. Watch out for common gotchas like losing this, forgetting to return, and unexpected arguments like with parseInt.

  9. Don't overuse HOFs. Sometimes a simple loop is clearer. Use HOFs when they make code more readable, not less.


Test Your Knowledge


Frequently Asked Questions


Callbacks

Callbacks are functions passed to higher-order functions

Content coming soon.

Map, Reduce, Filter

The most common built-in higher-order functions

Content coming soon.

Pure Functions

HOFs work best when combined with pure functions

Content coming soon.

Currying & Composition

Advanced patterns built on top of higher-order functions

Content coming soon.

Scope and Closures

Closures are what make functions returning functions work


Reference

Articles

Videos

AI assistant loads as you scroll.

Comments and synced upvotes load as you scroll.