Skip to main content

🧩 Higher-Order Function Patterns

When functions can be passed around and returned like any other value, a small set of reusable patterns emerges β€” map, compose, curry, and decorate. Master these and your code becomes shorter, more declarative, and easier to reuse.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define a higher-order function and explain what "functions as first-class values" means
  • Use the built-in HOFs β€” map, filter, reduce, find, some, every β€” fluently
  • Implement compose and pipe to chain transformations
  • Apply currying, partial application, decorators, and memoization
  • Recognize these patterns in React Hooks and Redux middleware

Estimated Time: 40–50 minutes  β€’  Difficulty: Intermediate

Hands-on: Build a mini functional toolkit (compose, curry) and a logging decorator.

In This Lesson

What Are Higher-Order Functions?

A higher-order function (HOF) is a function that does at least one of these: takes another function as an argument, or returns a function as its result. This is possible because in JavaScript functions are first-class values β€” you can store them in variables, pass them to other functions, and return them, exactly like numbers or strings.

🧩 A useful framing: Higher-order functions are "function managers." They usually don't do the low-level work themselves; they orchestrate which function runs and how, leaving you to supply the small piece of behavior that varies.
graph TD A[Higher-Order Function] --> B[Takes a function as input] A --> C[Returns a function as output] B --> B1[map Β· filter Β· reduce] C --> C1[Function factories] C --> C2[Currying] B --> D[Both at once] C --> D D --> D1[Composition & decorators]

Here are the two shapes side by side β€” one takes a function, the other returns one:

// 1) Takes a function as an argument
function applyOperation(x, y, operation) {
  return operation(x, y);
}
const add = (a, b) => a + b;
console.log(applyOperation(5, 3, add)); // 8

// 2) Returns a function
function createMultiplier(factor) {
  return (n) => n * factor;
}
const double = createMultiplier(2);
console.log(double(5)); // 10

Built-in HOFs

JavaScript's array methods are the higher-order functions you will use most. Each takes a function and applies it across the array, so you describe what you want rather than hand-writing a loop.

const numbers = [1, 2, 3, 4, 5];

numbers.map((x) => x * x);          // [1, 4, 9, 16, 25]  β€” transform
numbers.filter((x) => x % 2 === 0); // [2, 4]             β€” select
numbers.reduce((sum, x) => sum + x, 0); // 15             β€” accumulate
numbers.find((x) => x > 3);         // 4                  β€” first match
numbers.some((x) => x > 4);         // true               β€” any match?
numbers.every((x) => x > 0);        // true               β€” all match?

πŸ“– Quick reference

map β†’ new array of the same length, each element transformed.

filter β†’ new array with only the elements that pass a test.

reduce β†’ a single accumulated value (sum, object, another array…).

Chaining for real data work

Because map and filter each return a new array, you can chain them into a readable data pipeline. Here we pull the names of active admins over 25:

const users = [
  { id: 1, name: 'Alice', age: 28, active: true, roles: ['user', 'admin'] },
  { id: 2, name: 'Bob', age: 35, active: false, roles: ['user'] },
  { id: 3, name: 'Charlie', age: 24, active: true, roles: ['user'] },
  { id: 4, name: 'Diana', age: 42, active: true, roles: ['user', 'admin'] },
];

const activeAdmins = users
  .filter((u) => u.active)
  .filter((u) => u.age > 25)
  .filter((u) => u.roles.includes('admin'))
  .map((u) => u.name);

console.log(activeAdmins); // ['Alice', 'Diana']

// reduce shines for grouping and counting
const countByStatus = users.reduce((counts, u) => {
  const key = u.active ? 'active' : 'inactive';
  counts[key] = (counts[key] || 0) + 1;
  return counts;
}, {});
console.log(countByStatus); // { active: 3, inactive: 1 }

Composition & Pipe

Function composition combines small functions into a bigger one, where each function's output feeds the next function's input. It is how you build complex transformations out of simple, tested pieces.

Data flowing through composed functions An input x passes through function f, then g, then h, producing output y. The whole thing equals h of g of f of x. x f g h y y = h(g(f(x)))
Figure 1 β€” compose runs right-to-left (h∘g∘f); pipe runs left-to-right, matching reading order.
// compose: right-to-left
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

// pipe: left-to-right (usually more readable)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const double = (x) => x * 2;
const increment = (x) => x + 1;
const square = (x) => x * x;

const f1 = compose(square, increment, double);
console.log(f1(3)); // 49  β€” square(increment(double(3)))

const f2 = pipe(double, increment, square);
console.log(f2(3)); // 49  β€” same result, read top to bottom

πŸ’‘ compose vs. pipe

They do the same job in opposite directions. pipe(a, b, c) reads as "do a, then b, then c," which most people find clearer. Libraries like Ramda and Lodash/fp ship both.

Currying & Partial Application

Currying transforms a function of many arguments into a chain of functions that each take one argument. Partial application is the closely related idea of fixing some arguments now and supplying the rest later. Both let you build specialized functions from general ones.

// A general curry helper
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...more) => curried.apply(this, args.concat(more));
  };
}

const add3 = (a, b, c) => a + b + c;
const curriedAdd = curry(add3);

console.log(curriedAdd(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

Currying pays off when the first argument is the "configuration" and the last is the "data." You fix the config once and get a reusable, data-ready function:

const filterBy = curry((predicate, array) => array.filter(predicate));

const onlyEven = filterBy((x) => x % 2 === 0);
console.log(onlyEven([1, 2, 3, 4, 5, 6])); // [2, 4, 6]

// Partial application with the native bind (or a spread helper)
const partial = (fn, ...fixed) => (...rest) => fn(...fixed, ...rest);

const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice')); // "Hello, Alice!"

πŸ“– Definition

Arity: the number of arguments a function expects (available as fn.length). Currying works by comparing how many arguments have arrived against the original arity.

Decorators & Memoization

A decorator is a higher-order function that wraps another function to add behavior β€” logging, timing, caching, error handling β€” without changing the original's core job. The wrapper takes the same arguments, does something extra, and delegates.

// Logging decorator
function withLogging(fn) {
  return function (...args) {
    console.log(`Calling ${fn.name}(${args.join(', ')})`);
    const result = fn(...args);
    console.log(`  β†’ returned ${result}`);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
// Calling add(2, 3)
//   β†’ returned 5

// Timing decorator
function withTiming(fn) {
  return function (...args) {
    const start = performance.now();
    const result = fn(...args);
    console.log(`${fn.name} took ${(performance.now() - start).toFixed(2)}ms`);
    return result;
  };
}

Memoization as a decorator

Memoization is a caching decorator: it stores results keyed by arguments so repeat calls are instant. It is the fix for the famously slow naive Fibonacci:

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(...args);
    cache.set(key, result);
    return result;
  };
}

const fib = memoize(function fib(n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
});

console.log(fib(40)); // 102334155 β€” fast, because each n is computed once

⚠️ Memoization caveats

Only memoize pure functions (same input β†’ same output, no side effects). The cache grows over time, so be mindful of memory for functions with many distinct inputs, and remember JSON.stringify keys ignore argument order differences only when the values serialize identically.

Real-World Patterns

Custom React Hooks

A custom Hook is a higher-order pattern: a function that composes built-in Hooks and returns reusable stateful logic. Here useFetch encapsulates loading, data, and error handling:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((json) => { setData(json); setLoading(false); })
      .catch((err) => {
        if (err.name !== 'AbortError') { setError(err.message); setLoading(false); }
      });

    return () => controller.abort(); // cleanup cancels in-flight request
  }, [url]);

  return { data, loading, error };
}

Redux middleware

Redux middleware is the classic triple-arrow HOF: a function returning a function returning a function. Each layer captures what it needs via closure.

// store => next => action => ...
const logger = (store) => (next) => (action) => {
  console.log('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState());
  return result;
};

Safe wrappers with error handling

function withErrorHandling(fn, onError) {
  return function (...args) {
    try {
      return fn(...args);
    } catch (error) {
      return onError(error, ...args);
    }
  };
}

const parse = withErrorHandling(JSON.parse, () => ({}));
console.log(parse('{"ok":true}')); // { ok: true }
console.log(parse('not json'));    // {} β€” falls back instead of throwing

Hands-on Exercise

πŸ‹οΈ Build a Mini Functional Toolkit

Objective: Implement the core HOFs yourself so the patterns become muscle memory.

Instructions:

  1. Write pipe(...fns) that returns a function running its stages left-to-right.
  2. Write a curry(fn) helper (or reuse the one above) and curry a 3-argument function.
  3. Write a withCount(fn) decorator that returns a wrapped function which also exposes how many times it has been called.
  4. Use pipe to build cleanTitle: trim, lowercase, then capitalize the first letter. Test it on " HELLO WORLD ".
πŸ’‘ Hint

pipe is (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x). For withCount, keep a let calls = 0; in the closure and attach a .calls getter or return an object.

βœ… Sample solution
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

const curry = (fn) => {
  return function curried(...args) {
    return args.length >= fn.length
      ? fn(...args)
      : (...more) => curried(...args, ...more);
  };
};

function withCount(fn) {
  let calls = 0;
  const wrapped = (...args) => { calls++; return fn(...args); };
  wrapped.getCalls = () => calls;
  return wrapped;
}

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);

const cleanTitle = pipe(trim, lower, capitalize);
console.log(cleanTitle('  HELLO WORLD  ')); // "Hello world"

const countedClean = withCount(cleanTitle);
countedClean('  A  ');
countedClean('  B  ');
console.log(countedClean.getCalls()); // 2

Best Practices

βœ… Do

  • Prefer map/filter/reduce over manual loops when transforming data β€” intent is clearer.
  • Keep the functions you pass small, named, and pure so they compose cleanly.
  • Use pipe for readability; reach for curry/partial to make configuration reusable.
  • Give wrapped/decorated functions meaningful names for readable stack traces.

⚠️ Don't

  • Don't overuse point-free composition until it becomes unreadable β€” clarity beats cleverness.
  • Don't memoize impure functions or functions with unbounded distinct inputs.
  • Don't reach for reduce when a plain map or filter expresses the intent better.
  • Don't mutate the array or objects inside a map/filter callback β€” return new values instead.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A higher-order function takes and/or returns functions β€” possible because functions are first-class values.
  • map/filter/reduce replace hand-written loops with declarative intent.
  • compose and pipe chain small functions into bigger transformations.
  • Currying, partial application, decorators, and memoization are all HOF patterns.
  • React custom Hooks and Redux middleware are these patterns in production.

🎯 Quick Quiz

Question 1: Which statement best defines a higher-order function?

Question 2: What does pipe(double, increment)(3) return, given double = x=>x*2 and increment = x=>x+1?

Question 3: When is memoization a safe optimization?

πŸ“š Further Reading

πŸš€ What's Next?

Higher-order functions are the toolkit; functional programming is the philosophy that ties them together with pure functions and immutability. That's the next lesson.

πŸŽ‰ Great progress!

You can now compose, curry, and decorate. Let's zoom out to the paradigm that uses them.