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
- Functions as arguments & return values
- Building Function Factories
- Currying & Partial Application
- Creating reusable logic pipelines
- Real-world HOF patterns (Middleware, Hooks)
- Memoization with HOFs
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
🎯 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:
- Takes another function as an argument
- Returns a function
- Or does both
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!
- ❌ Don't make everything curried "just because"
- ❌ Avoid over-nesting—deeply nested currying becomes unreadable
- ✔ Curry only when partial reuse is useful
🏗️ 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
- React hooks (useState, useCallback, useReducer)
- Express middleware
- MongoDB query builders
- AI prompt pipelines
- Middleware stacks
- Event handler factories
- Redux store logic
- Functional data cleaning
- Web scrapers & automation tools
- TypeScript utility types
🎯 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:
- Predictability: Small, pure functions with no hidden side effects
- Composability: Functions that snap together like building blocks
- Reusability: No rewriting logic for every new feature
- Scalability: Easier growth as projects evolve
- Cleaner abstractions: Intent becomes clearer than traditional imperative code
- Reduced bugs: Fewer hidden dependencies, fewer surprises
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
- Previous: Functional Programming in JavaScript
- Next: Pure Functions, Immutability & Side Effects — Write predictable code by controlling side effects and avoiding mutation
- Quick reference: JavaScript cheat sheet