learn.colinkim.dev

Higher-order functions

Learn how functions can accept and return other functions, and why this pattern is the backbone of JavaScript's most useful abstractions.

In JavaScript, functions are first-class values. They can be:

  • assigned to variables
  • passed as arguments to other functions
  • returned from functions
  • stored in objects and arrays

A higher-order function is any function that takes a function as an argument, returns a function, or both. This is not a special language feature — it is just a consequence of functions being first-class values.

Functions as arguments

The most common higher-order function pattern is passing a function as a callback:

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

numbers.forEach((n) => console.log(n));

forEach is a higher-order function. It does not know what you will do with each element — it delegates that to the function you pass in.

Array methods are the most visible family of higher-order functions, covered extensively in the data manipulation unit. But the pattern appears everywhere:

// setTimeout accepts a function to call later
setTimeout(() => {
  console.log("Delayed message");
}, 1000);

// addEventListener accepts a function to call on events
button.addEventListener("click", (event) => {
  console.log("Clicked:", event.target);
});

Functions returning functions

Functions can also produce other functions as their output:

function createGreeter(greeting) {
  return (name) => `${greeting}, ${name}!`;
}

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");

sayHello("Ada");   // "Hello, Ada!"
sayHi("Grace");    // "Hi, Grace!"

The outer function configures the inner one. Each call to createGreeter produces an independent function with its own captured greeting.

Why higher-order functions matter

Higher-order functions enable abstraction over behavior, not just data. Instead of writing the same loop structure with different logic inside, you pass the varying logic as a function.

Compare these two approaches:

// Without higher-order functions — repetitive structure
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

const evens = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) evens.push(numbers[i]);
}

// With higher-order functions — structure is abstracted
const doubled = numbers.map((n) => n * 2);
const evens = numbers.filter((n) => n % 2 === 0);

The second version communicates intent. map says “transform each element.” filter says “keep only matching elements.” The loop mechanics are hidden; the business logic is visible.

Building your own higher-order functions

You do not need to wait for array methods to use this pattern. Here is a utility that retries a function call:

function retry(fn, times, delay) {
  return function(...args) {
    for (let i = 0; i < times; i++) {
      try {
        return fn(...args);
      } catch (error) {
        if (i === times - 1) throw error;
        // could add delay here
      }
    }
  };
}

const fetchData = retry(() => {
  // might throw on bad connection
  return getDataFromAPI();
}, 3);

fetchData();

And a function that creates a debounced version of another function:

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const handleSearch = debounce((query) => {
  fetchResults(query);
}, 300);

// Rapid calls only execute the last one
handleSearch("a");
handleSearch("ab");
handleSearch("abc");  // only this one runs, after 300ms

Both of these use closures to capture and retain state (timer, times, delay) that the returned function uses on each call.

Composition

Higher-order functions also enable composition — combining small functions into larger pipelines:

function compose(f, g) {
  return (x) => f(g(x));
}

const toUpper = (s) => s.toUpperCase();
const trim = (s) => s.trim();
const clean = compose(toUpper, trim);

clean("  hello  ");  // "HELLO"

The composed function applies trim first, then toUpper. You build behavior by combining focused functions rather than writing one large function that does everything.

What to carry forward

  • functions are first-class values in JavaScript — they can be passed, returned, and stored
  • a higher-order function takes a function as an argument, returns a function, or both
  • array methods like .map(), .filter(), and .reduce() are higher-order functions
  • higher-order functions abstract behavior, not just data
  • returning functions from functions lets you create configured, specialized utilities
  • composition combines small functions into larger pipelines

Higher-order functions are the mechanism behind most of JavaScript’s most useful patterns. The next unit covers the data manipulation methods that put higher-order functions to work.

Progress

Quick checks

No quick checks in this lesson.

Mark lesson manually or answer quick checks to track progress.