Higher-Order Functions

Med

A higher-order function (HOF) is a function that takes a function as an argument, returns a function, or both. HOFs are the backbone of functional programming in JavaScript — they enable abstraction, code reuse, and declarative patterns. Beyond array methods, HOFs power middleware, decorators, function factories, and compose/pipe utilities.

Interactive Visualization

for...in vs for...of

Step 1: Setup
const obj = { a: 1, b: 2 };
const arr = [10, 20, 30];
Output
No output yet
1 / 5

Key Points

  • A HOF takes a function as an argument or returns a function (or both)
  • Function factories create specialized functions from general ones using closures
  • The middleware pattern chains HOFs where each can modify input, output, or control flow
  • Decorators wrap a function to add behavior without modifying the original
  • compose(f, g)(x) applies right-to-left: f(g(x)); pipe applies left-to-right
  • HOFs enable separation of "what to do" from "how to iterate"

Code Examples

Function Factory

function createMultiplier(factor) {
  return function (number) {
    return number * factor
  }
}

const double = createMultiplier(2)
const triple = createMultiplier(3)

console.log(double(5))  // 10
console.log(triple(5))  // 15

const prices = [9.99, 24.50, 3.75]
console.log(prices.map(double)) // [19.98, 49, 7.5]

createMultiplier returns a new function. Each returned function closes over its own factor value.

Decorator Pattern

function withLogging(fn) {
  return function (...args) {
    console.log('Calling ' + fn.name + ' with', args)
    const result = fn(...args)
    console.log('Result:', result)
    return result
  }
}

function add(a, b) { return a + b }
const loggedAdd = withLogging(add)
loggedAdd(2, 3) // logs call + result, returns 5

Decorators wrap a function to add cross-cutting concerns. The original function is unchanged.

Compose and Pipe

const compose = (...fns) =>
  (x) => fns.reduceRight((acc, fn) => fn(acc), x)

const pipe = (...fns) =>
  (x) => fns.reduce((acc, fn) => fn(acc), x)

const trim = (s) => s.trim()
const toLower = (s) => s.toLowerCase()
const split = (s) => s.split(' ')
const unique = (arr) => [...new Set(arr)]

const process = pipe(trim, toLower, split, unique)
console.log(process('  Banana Apple banana  '))
// ['banana', 'apple']

compose reads right-to-left, pipe reads left-to-right. Both combine small functions into a pipeline.

Custom Retry HOF

function withRetry(fn, maxAttempts, delayMs) {
  return async function (...args) {
    let lastError
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await fn(...args)
      } catch (err) {
        lastError = err
        if (attempt < maxAttempts) {
          await new Promise(r => setTimeout(r, delayMs * attempt))
        }
      }
    }
    throw lastError
  }
}

const resilientFetch = withRetry(fetchUser, 3, 1000)

withRetry adds retry logic to any async function without modifying the original.

Common Mistakes

  • Creating overly abstract HOFs that are harder to understand than the code they replace
  • Forgetting to preserve function name and length when wrapping
  • Not handling both sync and async return values in decorators
  • Confusing a HOF with a method that happens to accept a callback

Interview Tips

  • Give examples beyond map/filter: function factories, decorators, middleware
  • Implement compose or pipe from scratch — interviewers love seeing reduce used elegantly
  • Show a practical decorator like withRetry or withCaching
  • HOFs enable the open-closed principle: extend behavior without modifying existing code

Related Concepts