Skip to main content

What are higher-order functions in JavaScript (hof)

Higher-order function (HOF) - a function that takes another function as an argument, returns a function as a result, or does both.

Theory

TL;DR

  • Functions in JavaScript are values - pass them around like numbers or strings
  • HOF = takes a function as input, or returns one, or both
  • Built-in examples you already know: map, filter, reduce, forEach
  • Use HOF when the same operation repeats with different behavior - pass the behavior in instead of copying code
  • Decision rule: if you catch yourself writing the same loop with a slightly different operation inside, a HOF probably belongs there

Quick example

javascript
// map() is a HOF - accepts a function, applies it to each element const numbers = [1, 2, 3]; const doubled = numbers.map(num => num * 2); // [2, 4, 6] // multiplier() is a HOF - returns a new configured function function multiplier(factor) { return function(number) { return number * factor; }; } const double = multiplier(2); console.log(double(5)); // 10

map takes num => num * 2 as an argument. multiplier builds and returns a new function configured with factor. Both are HOFs.

Why HOFs exist

Without HOFs, you repeat logic. With them, you write it once and pass in what changes. That is the whole point.

Consider transforming an array: without map, you write a for loop every time, declare a new array, manually push results. With map, you say "here is what to do per item" and it handles the rest. The operation stays consistent; the behavior you inject varies.

I have seen developers copy-paste the same for loop five times with slightly different transformations inside. A map call would have been one line each time.

This works because JavaScript treats functions as first-class citizens. They are objects stored on the heap, assignable to variables, passable as arguments, returnable from other functions. A HOF just uses that deliberately.

When to use

  • Transforming collections: map, filter, reduce for any list of API data or UI items
  • Event handling: addEventListener takes a callback - your function runs when the event fires
  • Async flows: Promise.then() takes a function to run on resolve
  • Express middleware: each middleware is a HOF that wraps the next handler with extra logic
  • Specialized functions: multiplier(2) gives you double, multiplier(3) gives you triple, from one definition

For simple one-off logic where a plain loop reads more clearly, skip the HOF.

How it works internally

V8 treats functions as objects. When you pass a function to a HOF, V8 copies a reference to that object, not the code itself. When a HOF returns a function, the returned function captures the outer scope through a closure - V8 allocates a closure cell on the heap holding references to lexical variables, keeping them alive as long as the inner function lives. Built-in methods like Array.prototype.map are implemented in C++ and call your JS callback on each iteration.

Common mistakes

Forgetting to call the returned function:

javascript
const doubler = multiplier(2); // Returns a function console.log(multiplier(2)); // Logs [Function] - not a number! console.log(doubler(5)); // 10 - this is what you wanted

multiplier(2) hands you a function. You still need to call it with the actual value.

Mutating the passed function:

javascript
// Bad: adds a property to the original function object function badHOF(fn) { fn.customProp = 'mutated'; // Changes caller scope return fn; } // Fine: wrap it, leave the original alone function goodHOF(fn) { return (...args) => fn(...args); }

Functions are objects. Adding properties to them affects the original outside your HOF.

Losing this context in callbacks:

javascript
const obj = { value: 42 }; [1, 2].forEach(function() { console.log(this.value); // undefined - context is lost }); [1, 2].forEach(function() { console.log(this.value); // 42 - explicit binding restores it }.bind(obj));

Unstable HOF references in React deps:

javascript
// Bad: new function reference every render triggers effect each time useEffect(() => { fetchData(increment); }, [increment]); // Good: useCallback stabilizes the reference const increment = useCallback(() => setCount(c => c + 1), []);

React compares dependencies by reference. A new function each render looks like a changed dependency and re-runs the effect.

Real-world usage

  • Array.map/filter/reduce: every React list render, Lodash data pipelines
  • Express middleware: app.use(authMiddleware(handler)) - each middleware wraps the next handler
  • Promise.then(): chains async work by passing handlers
  • React useCallback: memoizes a function to keep its reference stable between renders
  • Lodash _.curry / _.partial: build specialized functions from general ones

Follow-up questions

Q: What is the difference between a HOF and a callback?
A: A callback is any function passed as an argument. A HOF is the function that receives it. map is the HOF; num => num * 2 is the callback.

Q: Implement map from scratch.
A:

javascript
function myMap(arr, fn) { const result = []; for (let i = 0; i < arr.length; i++) { result.push(fn(arr[i], i, arr)); } return result; }

Q: How do closures relate to HOFs?
A: Most HOFs that return functions rely on closures. The returned function keeps access to variables from the outer scope - like factor inside multiplier - even after the outer call has finished.

Q: Do HOFs have a performance cost?
A: In tight loops, yes. Returned functions create closure allocations on the heap. For 1M+ items, a manual loop can run 2-3x faster in V8. In most application code that threshold does not matter.

Q: In React 18 concurrent mode, why does an un-memoized HOF in useEffect deps cause an infinite loop?
A: Each render creates a new function reference. React compares dependencies by reference, so a new function looks like a changed dep. The effect re-runs, triggers another render, which creates another new function. useCallback with a correct deps array breaks the cycle.

Examples

Basic: wrapping any function with a logger

javascript
function withLogging(fn) { return (...args) => { console.log('Calling:', fn.name, 'with args:', args); const result = fn(...args); console.log('Returned:', result); return result; }; } function add(a, b) { return a + b; } const loggedAdd = withLogging(add); loggedAdd(3, 4); // Calling: add with args: [3, 4] // Returned: 7

withLogging returns a new function that wraps whatever you pass in. The original add is untouched. This is the decorator pattern: extend behavior without changing the source function.

Intermediate: Express route validation middleware

javascript
const express = require('express'); const app = express(); app.use(express.json()); // HOF: accepts a route handler, returns a new handler with validation built in function validateUser(fn) { return (req, res, next) => { if (!req.body.username) { return res.status(400).send('Missing username'); } return fn(req, res, next); }; } const userHandler = (req, res) => res.json({ user: req.body.username }); app.post('/user', validateUser(userHandler)); // POST /user { username: 'alice' } => 200 { user: 'alice' } // POST /user {} => 400 "Missing username"

validateUser wraps any route handler with a validation check. The handler itself knows nothing about validation. Swap in requireAdmin or checkRateLimit and the pattern is identical.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?