Knowledge Base/concepts/Higher-Order Functions in JavaScript

Higher-Order Functions in JavaScript

Higher-order functions in JavaScript are not advanced because the definition is hard. A function is higher-order if it accepts another function, returns a function, or both. The difficulty is learning when that abstraction clarifies the code and when it hides the boring thing a loop would have said plainly.

Use higher-order functions to separate stable structure from behavior that changes.

The repetition problem

Imagine three functions that all walk through an array:

function doubleAll(numbers) {
  const result = [];

  for (const number of numbers) {
    result.push(number * 2);
  }

  return result;
}

function squareAll(numbers) {
  const result = [];

  for (const number of numbers) {
    result.push(number * number);
  }

  return result;
}

The loop structure is the same. Only the transformation changes. A higher-order function lets you pass the changing part in:

function transformAll(items, transform) {
  const result = [];

  for (const item of items) {
    result.push(transform(item));
  }

  return result;
}

const doubled = transformAll([1, 2, 3], (n) => n * 2);
const squared = transformAll([1, 2, 3], (n) => n * n);

That is the pattern in miniature: keep the iteration in one place, pass the behavior as a function.

What makes a function higher-order

A function is higher-order if it does at least one of these:

  • Takes a function as an argument.
  • Returns a function as its result.
function runTwice(fn) {
  fn();
  fn();
}

function makeMultiplier(factor) {
  return function multiply(number) {
    return number * factor;
  };
}

runTwice takes a function. makeMultiplier returns one. Both are higher-order.

A plain function that takes data and returns data is not higher-order:

function add(a, b) {
  return a + b;
}

Nothing mystical. Just functions as values.

Callbacks are passed-in behavior

When you pass a function into another function, that passed-in function is usually called a callback.

function repeat(count, callback) {
  for (let i = 0; i < count; i++) {
    callback(i);
  }
}

repeat(3, (index) => {
  console.log(`Run ${index}`);
});

The higher-order function controls when the callback runs. The caller controls what the callback does.

The browser uses this everywhere:

button.addEventListener("click", (event) => {
  console.log(event.target);
});

setTimeout(() => {
  console.log("Later");
}, 1000);

addEventListener and setTimeout are higher-order functions. You have been using the pattern even if the name sounded academic.

Returning functions creates configured behavior

Returning a function lets you create specialized functions from one general function.

function makePrefixer(prefix) {
  return function addPrefix(value) {
    return `${prefix}-${value}`;
  };
}

const makeCourseId = makePrefixer("course");
const makeExerciseId = makePrefixer("exercise");

console.log(makeCourseId("arrays")); // "course-arrays"
console.log(makeExerciseId("map")); // "exercise-map"

The returned function remembers prefix through a closure. For the mechanics, see Closures.

This pattern is useful when configuration should happen once and execution should happen many times.

Array methods are built-in higher-order functions

map, filter, and reduce are higher-order functions because they accept callbacks.

const products = [
  { name: "Course", price: 39, active: true },
  { name: "Workbook", price: 12, active: false },
  { name: "Exam pack", price: 19, active: true },
];

const activeNames = products
  .filter((product) => product.active)
  .map((product) => product.name);

console.log(activeNames); // ["Course", "Exam pack"]

The callbacks express the behavior that varies:

  • filter receives a predicate.
  • map receives a transform.
  • reduce receives an accumulator update.

The JavaScript Array Methods article covers the method choices in more detail.

Writing your own higher-order utility

Here is a small wrapper that logs function calls:

function withLogging(fn, label) {
  return function loggedFunction(...args) {
    console.log(`Calling ${label}`);
    return fn(...args);
  };
}

function add(a, b) {
  return a + b;
}

const loggedAdd = withLogging(add, "add");

console.log(loggedAdd(2, 3));
// Calling add
// 5

withLogging does not know what add does. It only knows how to wrap a function with a bit of behavior around it. That is where higher-order functions earn their keep.

Another useful example is once:

function once(fn) {
  let called = false;
  let result;

  return function runOnce(...args) {
    if (!called) {
      called = true;
      result = fn(...args);
    }

    return result;
  };
}

const initialize = once(() => {
  console.log("Initializing");
  return { ready: true };
});

initialize(); // logs "Initializing"
initialize(); // returns cached result

The returned function closes over called and result. That private state is the whole trick.

Composition can help or hurt

Composition means building larger behavior from smaller functions:

function pipe(...fns) {
  return function runPipeline(value) {
    return fns.reduce((current, fn) => fn(current), value);
  };
}

const slugify = pipe(
  (value) => value.trim(),
  (value) => value.toLowerCase(),
  (value) => value.replace(/\s+/g, "-"),
);

console.log(slugify("  JavaScript Arrays  "));
// "javascript-arrays"

That is readable because each step is small and named by its code.

Composition becomes irritating when the reader has to jump through five helpers to understand one line. Use this pattern for genuine reuse or genuinely clearer sequencing, not to make simple code look more important than it is.

Common mistakes

Forgetting to return from a callback:

const doubled = [1, 2, 3].map((n) => {
  n * 2;
});

console.log(doubled); // [undefined, undefined, undefined]

Mutating original objects inside map:

const updated = users.map((user) => {
  user.active = true;
  return user;
});

That changes the original user objects. Return new objects instead:

const updated = users.map((user) => ({
  ...user,
  active: true,
}));

Passing a function that expects different arguments:

["1", "2", "3"].map(parseInt);
// [1, NaN, NaN]

map passes (value, index, array). parseInt treats the second argument as the radix, so the index becomes the radix. Wrap it:

["1", "2", "3"].map((value) => parseInt(value, 10));

When a loop is clearer

Higher-order functions are not a purity test. Use a loop when you need early exit, sequential await, or several related updates in one place.

for (const job of jobs) {
  if (job.cancelled) continue;

  const result = await runJob(job);

  if (!result.ok) break;
}

Forcing this into callbacks would make it harder to read. The JavaScript for...of Loop guide covers that control-flow shape.

Key Takeaways

  • A higher-order function accepts a function, returns a function, or both.
  • Callbacks let callers provide behavior while the higher-order function owns the structure.
  • Returning functions is useful for configuration, wrappers, factories, and private state.
  • map, filter, reduce, addEventListener, and setTimeout are everyday higher-order functions.
  • Use higher-order functions when they clarify intent. Use loops when control flow is the real story.
Share this article:
javascriptfunctionsfundamentalsintermediate