Higher-Order Functions

Reviewed & published by Brayan K

Master one of JavaScript's most powerful patterns—used everywhere from React hooks to Node.js middleware, AI pipelines, and modern framework architecture.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

🎯 What Are Higher-Order Functions?

Higher-order functions (HOFs) are one of the most powerful ideas in JavaScript and form the backbone of modern development across front-end frameworks (React, Vue, Svelte), backend systems (Node.js), and even machine-learning pipelines. A higher-order function is any function that:

At their core, HOFs allow you to treat functions as first-class values. This means you can store them in variables, pass them into other functions, return them from functions, add them to objects, and even generate them dynamically.

📝 Example: Passing Functions as Arguments

This pattern powers countless libraries—Express uses HOFs for middleware, React uses them for hooks and components, Node uses them for callbacks.

function applyOperation(a, b, operation) {
  return operation(a, b);
}

const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

console.log(applyOperation(5, 3, add));      // 8
console.log(applyOperation(5, 3, multiply)); // 15

🏭 Returning Functions (Function Factories)

A higher-order function can also produce a new function. This is extremely useful for configuration—how analytics systems, validators, and pricing engines are typically built.

function makeTaxCalculator(rate) {
  return amount => amount * rate;
}

const ukTax = makeTaxCalculator(0.20);
const euTax = makeTaxCalculator(0.23);

console.log(ukTax(1000));  // 200
console.log(euTax(1000));  // 230

🔬 Worked Example: Both Directions in One Program

Read this one before writing anything of your own. It puts all three shapes of higher-order function side by side in a single runnable program: one that takes a function, one that returns a function, and one that does both. Every non-obvious line is commented with what it produces.

// Higher-order functions, both directions, in one program.
// "Higher-order" only means: a function that TAKES a function, RETURNS a
// function, or does both. Nothing more exotic than that.

// ---- Direction 1: TAKES a function ---------------------------------
// repeat has no idea what the work is, and does not need to. The loop is
// fixed; the behaviour is handed in from outside.
function repeat(times, action) {
  for (let i = 1; i <= times; i++) {
    action(i);          // "action" is whatever function you passed in
  }
}

repeat(3, n => console.log("tick " + n));   // prints tick 1, tick 2, tick 3

// ---- Direction 2: RETURNS a function -------------------------------
// makeTagger takes a value NOW and hands back a function you call LATER.
// The returned arrow remembers "tag" through closure.
function makeTagger(tag) {
  return message => "[" + tag + "] " + message;
}

const warn  = makeTagger("WARN");    // a brand new function...
const debug = makeTagger("DEBUG");   // ...and another, with its own tag

console.log(warn("disk almost full"));   // [WARN] disk almost full
console.log(debug("cache miss"));        // [DEBUG] cache miss

// ---- Both at once ---------------------------------------------------
// withCount takes a function AND returns a function: a wrapped version
// that counts its own calls. The original function is never modified.
function withCount(fn) {
  let calls = 0;

  const wrapped = (...args) => {
    calls++;              // the extra behaviour
    return fn(...args);   // then the original work, untouched
  };

  wrapped.callCount = () => calls;   // a small extra handle on the wrapper
  return wrapped;
}

const shout = withCount(text => text.toUpperCase());

console.log(shout("hello"));                        // HELLO
console.log(shout("again"));                        // AGAIN
console.log("shout ran", shout.callCount(), "times");

// Those three shapes are the whole idea. Array methods, React hooks,
// Express middleware and test spies are all one of them wearing a hat.

// ✅ Expected output:
// tick 1
// tick 2
// tick 3
// [WARN] disk almost full
// [DEBUG] cache miss
// HELLO
// AGAIN
// shout ran 2 times

✨ Why Higher-Order Functions Matter

Less duplication, more clarity

Write once, use everywhere

❌ "I'll just copy-paste logic instead of wrapping it in a function."

This leads to massive technical debt. ✔ HOFs eliminate repetition and force structure.

🔄 Introduction to Currying

Currying transforms a function that takes multiple arguments into a sequence of single-argument functions. This allows functions to be partially applied—meaning you can "pre-load" some arguments and reuse the function later with different values.

This looks strange at first, but currying is simply delaying execution until all arguments are provided.

📝 Basic Example of Currying

const curryAdd = a => b => c => a + b + c;

console.log(curryAdd(2)(5)(7)); // 14

🎯 Partial Application

Partial application = fixing some arguments while leaving the rest open. This creates mini-functions with reusable behaviour.

const multiply = a => b => a * b;

const double = multiply(2);
const triple = multiply(3);

console.log(double(10));  // 20
console.log(triple(10));  // 30

// Perfect for pricing & tax logic, unit conversion,
// formatting text, analytics, event handlers, etc.

⚙️ Currying for Configuration

Very common in React & Node—create reusable "message builders" without repeating yourself.

🎯 Your Turn: A Curried Validator

Time to write some yourself. Each blank below is one higher-order idea: naming the parameter of a returned function, calling a method on the value that arrives later, and choosing the array method that only says yes when every check passes. Fill in each ___ and compare with the expected output at the bottom.

// 🎯 YOUR TURN — build a curried username validator
// Replace each ___ then press Run.

// 1) minLength takes a number NOW and returns a function that takes a
//    string LATER. The body already refers to that later value as "value" —
//    give the returned function that parameter name.
const minLength = n => ___ => value.length >= n;        // 👉 name the later parameter

// 2) startsWith takes a prefix NOW and returns a checker for LATER.
const startsWith = prefix => value => value.___(prefix);  // 👉 which String method?

// 3) combine takes an ARRAY OF CHECK FUNCTIONS and returns one function
//    that is true only when every single check says true.
const combine = checks => value => checks.___(check => check(value));   // 👉 which array method?

const isValidUsername = combine([minLength(5), startsWith("dev_")]);

console.log(isValidUsername("dev_ada"));     // long enough AND right prefix
console.log(isValidUsername("dev_"));        // right prefix, too short
console.log(isValidUsername("admin_ada"));   // long enough, wrong prefix

// ✅ Expected output:
// true
// false
// false

🚀 Currying in Functional Pipelines

Currying lets you configure part of the function early and use the rest later—perfect for building data transformation pipelines.

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

const add = a => b => b + a;
const multiply = a => b => b * a;

const transform = pipe(
  multiply(10),
  add(5)
);

console.log(transform(3)); // 35

💡 Pro Tip: Currying is a tool, not a religion!

🏗️ Automatic Currying Function

Create a customised price engine with tax and discount pre-configured. This is how e-commerce platforms, SaaS billing systems, game engines, and AI pipelines reuse complex logic.

const curry = fn => 
  (...args) => 
    args.length >= fn.length 
      ? fn(...args) 
      : (...next) => curry(fn)(...args, ...next);

const calculate = (tax, discount, price) =>
  price * tax - discount;

const config = curry(calculate)(1.2)(5);

console.log(config(100)); // 115
console.log(config(200)); // 235

🎨 Real-World Example: DOM Events

Currying is extremely useful in UI-driven development or event handlers—creates reusable, predictable event pipelines.

const handleEvent = type => element => callback => {
  element.addEventListener(type, callback);
};

const onClick = handleEvent("click");

// Usage (won't work in this editor, but try in browser):
// onClick(document.querySelector("#btn"))(() => 
//   console.log("Clicked!")
// );

console.log("Event handler factory created!");
console.log("Use this pattern in real DOM environments.");

🔐 Backend Authentication Logic

This pattern turns messy nested conditionals into readable composable logic—perfect for configuration-heavy backend systems.

const authenticate =
  role =>
  permissions =>
  user =>
    user.role === role && 
    permissions.every(p => user.permissions.includes(p));

const isAdmin = authenticate("admin")(["read", "write", "delete"]);

console.log(isAdmin({ 
  role: "admin", 
  permissions: ["read", "write", "delete"] 
})); // true

console.log(isAdmin({ 
  role: "user", 
  permissions: ["read"] 
})); // false

⚡ Building Reusable Pipelines

Real power emerges when higher-order functions and currying work together. This pattern is functionally identical to what large-scale libraries like RxJS, Ramda, and Lodash/fp use internally.

const map = fn => arr => arr.map(fn);
const filter = fn => arr => arr.filter(fn);
const reduce = (fn, init) => arr => arr.reduce(fn, init);

const double = x => x * 2;
const isEven = x => x % 2 === 0;

// Read this from the inside out: filter first, then double, then rebuild.
const process = arr =>
  reduce((acc, x) => [...acc, x], [])(
    map(double)(
      filter(isEven)(arr)
    )
  );

console.log(process([1,2,3,4,5,6])); // [4, 8, 12]
// [1,2,3,4,5,6] -> keep evens [2,4,6] -> double each [4,8,12]

💾 Memoization with Currying

Curried functions naturally fit memoization because they separate arguments across stages—critical for performance-sensitive tasks like UI rendering, animations, or state management.

💰 Configurable Business Logic

Instead of hardcoding values, currying allows customization—avoids "magic numbers" and makes future adjustments painless.

const calculateFee =
  rate =>
  min =>
  amount =>
    Math.max(amount * rate, min);

const transactionFee = calculateFee(0.015)(2);

console.log(transactionFee(100));  // 2 (minimum applied)
console.log(transactionFee(1000)); // 15 (rate applied)

🌍 Where HOFs + Currying Appear

🎯 Mini-Challenge: Write Two Decorators

A decorator is a higher-order function that takes a function and hands back a new one with extra behaviour wrapped around it — the original is left alone. once and withLogging are two you will meet in real codebases: one guards an expensive setup step, the other is how test spies and debug tooling work.

Nothing is filled in this time. You get the brief and a fixed test drive — if your output matches the expected output, both decorators are correct.

// 🎯 MINI-CHALLENGE: two decorators
//
// Write both from scratch. Only the brief is here.
//
// 1. once(fn)
//    Returns a wrapped function that calls fn on the FIRST call only.
//    Every later call returns that same first result without calling fn again.
//
// 2. withLogging(name, fn)
//    Returns a wrapped function that prints
//        calling NAME with A,B          (the arguments joined by a comma)
//    then calls fn, then prints
//        NAME returned R
//    and finally returns R.
//
// Neither decorator may modify the original function.
// Hint: both need to accept any number of arguments — (...args) and fn(...args).

// your code here


// --- Do not change the code below: this is the test drive ---
let initCount = 0;
const init = once(() => { initCount++; return "ready"; });

console.log(init());
console.log(init());
console.log("init ran", initCount, "time(s)");

const add = withLogging("add", (a, b) => a + b);
const result = add(2, 3);
console.log("result is", result);

// ✅ Expected output:
// ready
// ready
// init ran 1 time(s)
// calling add with 2,3
// add returned 5
// result is 5

🎯 Key Takeaway

Higher-order functions and currying continue to dominate modern JavaScript ecosystems because they provide:

Mastering higher-order functions and currying means mastering how modern JavaScript thinks. Developers who understand these patterns write cleaner, faster, and far more maintainable code.

Practice quiz

What qualifies a function as a higher-order function?

  • It is async
  • It uses recursion
  • It takes a function as an argument, returns a function, or both
  • It is defined with the function keyword

Answer: It takes a function as an argument, returns a function, or both. A higher-order function takes another function as an argument, returns a function, or does both.

What does it mean that functions are 'first-class values' in JavaScript?

  • They can be stored in variables, passed, returned, and generated dynamically
  • They run before other code
  • They are faster than methods
  • They cannot be reassigned

Answer: They can be stored in variables, passed, returned, and generated dynamically. First-class means functions can be stored, passed, returned, and created like any other value.

Given function applyOperation(a, b, op) { return op(a, b); } and add = (x,y)=>x+y, what is applyOperation(5, 3, add)?

  • 53
  • 15
  • 2
  • 8

Answer: 8. applyOperation calls add(5, 3), which returns 8.

What does the curried function curryAdd = a => b => c => a + b + c return for curryAdd(2)(5)(7)?

  • 257
  • 14
  • 12
  • 70

Answer: 14. Each call supplies one argument; 2 + 5 + 7 equals 14.

What is currying?

  • Transforming a multi-argument function into a sequence of single-argument functions
  • Caching function results
  • Calling a function repeatedly
  • Combining two arrays

Answer: Transforming a multi-argument function into a sequence of single-argument functions. Currying turns a multi-argument function into a chain of single-argument functions.

What is partial application?

  • Running only part of a function body
  • Splitting a function into modules
  • Fixing some arguments while leaving the rest open to create reusable mini-functions
  • Applying a function to an array

Answer: Fixing some arguments while leaving the rest open to create reusable mini-functions. Partial application pre-loads some arguments, producing a reusable function awaiting the rest.

With const multiply = a => b => a * b, what does const double = multiply(2); double(10) return?

  • 12
  • 20
  • 210
  • 2

Answer: 20. double fixes a = 2, so double(10) computes 2 * 10 = 20.

What is a function factory?

  • A function that mutates global state
  • A class that builds objects
  • A loop that creates variables
  • A higher-order function that produces and returns a new configured function

Answer: A higher-order function that produces and returns a new configured function. A function factory returns a new function configured by the arguments you pass in, like makeTaxCalculator(rate).

What problem does memoization with a higher-order function solve?

  • Type coercion
  • It caches results so repeated calls with the same argument skip recomputation
  • It prevents callback hell
  • It enforces immutability

Answer: It caches results so repeated calls with the same argument skip recomputation. Memoization stores computed results in a cache and returns the cached value on a cache hit.

Given const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x), add = a=>b=>b+a, multiply = a=>b=>b*a, what does pipe(multiply(10), add(5))(3) return?

  • 45
  • 90
  • 35
  • 18

Answer: 35. multiply(10)(3) is 30, then add(5)(30) is 35; pipe applies the functions left to right.

Continue this course