Higher-Order Functions in JavaScript: Full Guide

Learn what higher-order functions are in JavaScript and how to use them effectively. Covers map, filter, reduce, forEach, sort, custom HOFs, function composition, and practical patterns for cleaner code.

JavaScriptbeginner
12 min read

A higher-order function is a function that takes another function as an argument, returns a function, or both. Higher-order functions are central to JavaScript programming - array methods like map(), filter(), and reduce() are all higher-order functions. Understanding them unlocks cleaner, more expressive, and more reusable code.

What Makes a Function "Higher-Order"?

In JavaScript, functions are first-class values. You can store them in variables, pass them as arguments, and return them from other functions. A higher-order function takes advantage of this by either:

  1. Accepting a function as an argument (like map, filter, addEventListener)
  2. Returning a function (like factory functions and closures)
javascriptjavascript
// Higher-order: accepts a function as argument
function repeat(times, action) {
  for (let i = 0; i < times; i++) {
    action(i);
  }
}
 
repeat(3, (i) => console.log(`Iteration ${i}`));
// Iteration 0
// Iteration 1
// Iteration 2
 
// Higher-order: returns a function
function createGreeter(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}
 
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayHi("Bob"));     // "Hi, Bob!"

First-Order vs Higher-Order

TypeDefinitionExample
First-order functionTakes only data as arguments, returns dataMath.max(1, 2, 3)
Higher-order functionTakes or returns a function[1,2,3].map(fn)

Built-In Higher-Order Functions

Array.prototype.map()

map() creates a new array by applying a function to every element:

javascriptjavascript
const numbers = [1, 2, 3, 4, 5];
 
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
 
const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 30 },
];
 
const names = users.map((user) => user.name);
console.log(names); // ["Alice", "Bob"]
 
// map with index
const indexed = ["a", "b", "c"].map((letter, i) => `${i}: ${letter}`);
console.log(indexed); // ["0: a", "1: b", "2: c"]

Array.prototype.filter()

filter() creates a new array with elements that pass a test:

javascriptjavascript
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
const evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
 
const users = [
  { name: "Alice", active: true },
  { name: "Bob", active: false },
  { name: "Charlie", active: true },
];
 
const activeUsers = users.filter((user) => user.active);
console.log(activeUsers.length); // 2

Array.prototype.reduce()

reduce() processes all elements and accumulates them into a single value:

javascriptjavascript
const numbers = [1, 2, 3, 4, 5];
 
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 15
 
// Count occurrences
const fruits = ["apple", "banana", "apple", "cherry", "banana", "apple"];
const counts = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, cherry: 1 }
 
// Group by property
const people = [
  { name: "Alice", department: "Engineering" },
  { name: "Bob", department: "Marketing" },
  { name: "Charlie", department: "Engineering" },
];
 
const byDepartment = people.reduce((groups, person) => {
  const dept = person.department;
  groups[dept] = groups[dept] || [];
  groups[dept].push(person);
  return groups;
}, {});

Array.prototype.forEach()

forEach() executes a function for each element but does not return a new array:

javascriptjavascript
const items = ["apple", "banana", "cherry"];
 
items.forEach((item, index) => {
  console.log(`${index + 1}. ${item}`);
});
// 1. apple
// 2. banana
// 3. cherry
forEach vs map

Use map() when you need a new array of transformed values. Use forEach() when you are performing side effects (logging, DOM updates) and do not need a return value. map() returns a new array; forEach() returns undefined.

Array.prototype.sort()

sort() accepts a comparison function:

javascriptjavascript
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
 
// Ascending
const ascending = [...numbers].sort((a, b) => a - b);
console.log(ascending); // [1, 1, 2, 3, 4, 5, 6, 9]
 
// Descending
const descending = [...numbers].sort((a, b) => b - a);
console.log(descending); // [9, 6, 5, 4, 3, 2, 1, 1]
 
// Sort objects by property
const users = [
  { name: "Charlie", age: 30 },
  { name: "Alice", age: 25 },
  { name: "Bob", age: 35 },
];
 
const byAge = [...users].sort((a, b) => a.age - b.age);
const byName = [...users].sort((a, b) => a.name.localeCompare(b.name));

Other Built-In HOFs

javascriptjavascript
const numbers = [1, 2, 3, 4, 5];
 
// find: returns first matching element
const firstEven = numbers.find((n) => n % 2 === 0); // 2
 
// findIndex: returns index of first match
const firstEvenIndex = numbers.findIndex((n) => n % 2 === 0); // 1
 
// some: returns true if any element passes
const hasNegative = numbers.some((n) => n < 0); // false
 
// every: returns true if all elements pass
const allPositive = numbers.every((n) => n > 0); // true
 
// flatMap: maps then flattens one level
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap((s) => s.split(" "));
// ["hello", "world", "foo", "bar"]

Chaining Higher-Order Functions

Chain multiple HOFs to build data transformation pipelines:

javascriptjavascript
const transactions = [
  { id: 1, amount: 250, type: "sale", date: "2024-01-15" },
  { id: 2, amount: -50, type: "refund", date: "2024-01-16" },
  { id: 3, amount: 120, type: "sale", date: "2024-02-01" },
  { id: 4, amount: 800, type: "sale", date: "2024-01-20" },
  { id: 5, amount: -30, type: "refund", date: "2024-02-05" },
];
 
const janSalesTotal = transactions
  .filter((t) => t.type === "sale")                    // only sales
  .filter((t) => t.date.startsWith("2024-01"))         // only January
  .map((t) => t.amount)                                // extract amounts
  .reduce((sum, amount) => sum + amount, 0);           // sum them
 
console.log(janSalesTotal); // 1050

Processing User Data Pipeline

javascriptjavascript
const rawUsers = [
  { name: "  alice  ", email: "ALICE@EXAMPLE.COM", active: true, age: 25 },
  { name: " bob ", email: "BOB@TEST.COM", active: false, age: 17 },
  { name: "charlie", email: "CHARLIE@EXAMPLE.COM", active: true, age: 30 },
  { name: " diana ", email: "DIANA@EXAMPLE.COM", active: true, age: 22 },
];
 
const processedUsers = rawUsers
  .filter((u) => u.active && u.age >= 18)
  .map((u) => ({
    name: u.name.trim().replace(/^\w/, (c) => c.toUpperCase()),
    email: u.email.toLowerCase(),
    ageGroup: u.age < 25 ? "young" : "adult",
  }))
  .sort((a, b) => a.name.localeCompare(b.name));
 
console.log(processedUsers);
// [
//   { name: "Alice", email: "alice@example.com", ageGroup: "adult" },
//   { name: "Charlie", email: "charlie@example.com", ageGroup: "adult" },
//   { name: "Diana", email: "diana@example.com", ageGroup: "young" }
// ]

Writing Custom Higher-Order Functions

Function That Accepts a Function

javascriptjavascript
// Custom retry logic
function retry(fn, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return fn();
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      console.log(`Attempt ${attempt} failed, retrying...`);
    }
  }
}
 
// Usage
const result = retry(() => {
  // some operation that might fail
  return JSON.parse('{"valid": true}');
}, 3);

Function That Returns a Function

javascriptjavascript
// Create a validator factory
function createValidator(rules) {
  return function (value) {
    const errors = [];
    for (const rule of rules) {
      const error = rule(value);
      if (error) errors.push(error);
    }
    return { isValid: errors.length === 0, errors };
  };
}
 
// Define rules as [callback functions](/tutorials/programming-languages/javascript/what-is-a-callback-function-in-js-full-tutorial)
const required = (v) => (!v ? "Field is required" : null);
const minLength = (min) => (v) => v.length < min ? `Minimum ${min} characters` : null;
const isEmail = (v) => !v.includes("@") ? "Invalid email" : null;
 
// Create validators
const validateEmail = createValidator([required, isEmail]);
const validatePassword = createValidator([required, minLength(8)]);
 
console.log(validateEmail("user@example.com")); // { isValid: true, errors: [] }
console.log(validateEmail(""));                  // { isValid: false, errors: ["Field is required", "Invalid email"] }
console.log(validatePassword("short"));          // { isValid: false, errors: ["Minimum 8 characters"] }

Timing Decorator

javascriptjavascript
function withTiming(fn, label = fn.name) {
  return function (...args) {
    const start = performance.now();
    const result = fn(...args);
    const duration = performance.now() - start;
    console.log(`${label} took ${duration.toFixed(2)}ms`);
    return result;
  };
}
 
function expensiveCalculation(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  return sum;
}
 
const timedCalc = withTiming(expensiveCalculation);
timedCalc(10000000); // "expensiveCalculation took 12.34ms"

Once (Run Only Once)

javascriptjavascript
function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}
 
const initialize = once(() => {
  console.log("Initializing...");
  return { ready: true };
});
 
initialize(); // "Initializing..." -> { ready: true }
initialize(); // (no log) -> { ready: true } (cached)
initialize(); // (no log) -> { ready: true } (cached)

Debounce

javascriptjavascript
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
// Only fires after the user stops typing for 300ms
const handleSearch = debounce((query) => {
  console.log(`Searching for: ${query}`);
}, 300);
 
// Simulating rapid input
handleSearch("h");
handleSearch("he");
handleSearch("hel");
handleSearch("hello"); // Only this one fires (after 300ms)

Function Composition

Combine pure functions into pipelines:

javascriptjavascript
// Compose: right to left
function compose(...fns) {
  return (x) => fns.reduceRight((value, fn) => fn(value), x);
}
 
// Pipe: left to right
function pipe(...fns) {
  return (x) => fns.reduce((value, fn) => fn(value), x);
}
 
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const replaceSpaces = (str) => str.replace(/\s+/g, "-");
const truncate = (str) => str.slice(0, 30);
 
// Create a slug generator by composing functions
const slugify = pipe(trim, toLowerCase, replaceSpaces, truncate);
 
console.log(slugify("  Hello World  "));     // "hello-world"
console.log(slugify("  JavaScript IIFE  ")); // "javascript-iife"

Composing With Multiple Arguments

javascriptjavascript
// Partial application for multi-argument functions
function partial(fn, ...presetArgs) {
  return function (...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}
 
function multiply(a, b) {
  return a * b;
}
 
const double = partial(multiply, 2);
const triple = partial(multiply, 3);
 
console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log([1, 2, 3, 4].map(double)); // [2, 4, 6, 8]

Practical Patterns

Event Handler Factory

javascriptjavascript
function createClickHandler(action, data) {
  return function (event) {
    event.preventDefault();
    console.log(`Action: ${action}`, data);
  };
}
 
// Each button gets its own handler with captured data
document.getElementById("save").addEventListener(
  "click",
  createClickHandler("save", { formId: "userForm" })
);
 
document.getElementById("delete").addEventListener(
  "click",
  createClickHandler("delete", { itemId: 42 })
);

Permission Checker

javascriptjavascript
function requiresRole(role) {
  return function (handler) {
    return function (request) {
      if (request.user.role !== role) {
        return { status: 403, message: "Forbidden" };
      }
      return handler(request);
    };
  };
}
 
const adminOnly = requiresRole("admin");
 
const deleteUser = adminOnly((request) => {
  return { status: 200, message: `Deleted user ${request.params.id}` };
});
 
// Test
console.log(deleteUser({ user: { role: "user" }, params: { id: 1 } }));
// { status: 403, message: "Forbidden" }
 
console.log(deleteUser({ user: { role: "admin" }, params: { id: 1 } }));
// { status: 200, message: "Deleted user 1" }

Array Utility Kit

javascriptjavascript
// Higher-order utility functions for arrays
const pluck = (key) => (arr) => arr.map((item) => item[key]);
const where = (predicate) => (arr) => arr.filter(predicate);
const sortBy = (key) => (arr) =>
  [...arr].sort((a, b) => (a[key] > b[key] ? 1 : -1));
const groupBy = (key) => (arr) =>
  arr.reduce((groups, item) => {
    const group = item[key];
    groups[group] = groups[group] || [];
    groups[group].push(item);
    return groups;
  }, {});
 
const users = [
  { name: "Charlie", department: "Engineering", age: 30 },
  { name: "Alice", department: "Marketing", age: 25 },
  { name: "Bob", department: "Engineering", age: 35 },
];
 
const getNames = pluck("name");
const getActive = where((u) => u.age > 25);
const sortByName = sortBy("name");
const groupByDept = groupBy("department");
 
console.log(getNames(users));       // ["Charlie", "Alice", "Bob"]
console.log(getActive(users));      // [Charlie, Bob]
console.log(sortByName(users));     // [Alice, Bob, Charlie]
console.log(groupByDept(users));    // { Engineering: [...], Marketing: [...] }

Common Mistakes

1. Forgetting to Return in map

javascriptjavascript
// BUG: arrow function with braces needs explicit return
const doubled = [1, 2, 3].map((n) => {
  n * 2; // missing return!
});
console.log(doubled); // [undefined, undefined, undefined]
 
// FIX: add return or use concise body
const doubled = [1, 2, 3].map((n) => n * 2);

2. Using map for Side Effects

javascriptjavascript
// WRONG: use forEach for side effects
[1, 2, 3].map((n) => console.log(n)); // creates unused array
 
// RIGHT: forEach for side effects
[1, 2, 3].forEach((n) => console.log(n));

3. Mutating in reduce

javascriptjavascript
// Risky: mutating the accumulator works but is confusing
const grouped = items.reduce((acc, item) => {
  acc[item.type] = acc[item.type] || [];
  acc[item.type].push(item); // mutating acc
  return acc;
}, {});
 
// This is a common pattern and acceptable, but be aware
// that the accumulator object is being mutated each iteration
Rune AI

Rune AI

Key Insights

  • Higher-order = takes or returns functions: map, filter, reduce are classic examples
  • Chain for transformations: .filter().map().reduce() builds clean data pipelines
  • Create factories: return functions with captured state for reusable logic
  • Compose small functions: pipe(fn1, fn2, fn3) for complex transformations
  • Use the right method: map for transformation, filter for selection, reduce for accumulation
  • Arrow functions for brevity: (x) => x * 2 is ideal for inline callbacks
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between map and forEach?

`map()` returns a new array with transformed values. `forEach()` returns `undefined` and is used for side effects only. Use `map()` when you need the result; use `forEach()` when you only need to perform actions.

When should I use reduce vs a [for loop](/tutorials/programming-languages/javascript/js-for-loop-syntax-a-complete-guide-for-beginners)?

Use `reduce()` when accumulating a single value from an array (sum, object, string). Use a `for` loop when the logic is complex, involves early termination, or needs side effects. If [reduce](/tutorials/programming-languages/javascript/javascript-functions-explained-from-basic-to-advanced-concepts) makes the code harder to read, use a loop.

Are arrow functions always better for HOFs?

[Arrow functions](/tutorials/programming-languages/javascript/javascript-arrow-functions-a-complete-es6-guide) are more concise and usually preferred for inline callbacks. However, they do not have their own `this`, so use [function expressions](/tutorials/programming-languages/javascript/javascript-function-expressions-vs-declarations) when you need `this` binding (such as in object methods or event handlers that reference the target element).

Can higher-order functions be async?

Yes. You can pass async functions to `map()`, `forEach()`, and custom HOFs. However, `map()` with async functions returns an array of Promises, not resolved values. Use `Promise.all(array.map(asyncFn))` to wait for all results.

Conclusion

Higher-order functions take functions as arguments, return functions, or both. JavaScript's built-in array methods (map, filter, reduce, sort, find, some, every) are all higher-order functions. Custom HOFs like factories, decorators, and validators make your code reusable and composable. Chain array HOFs to build data transformation pipelines. Use function composition (pipe/compose) to build complex operations from simple, pure functions.