Module 3 · Functions & Scope Mastery ⏱ 18 min

Higher-Order Functions

By the end of this lesson you will be able to:
  • Treat functions as values: pass them as arguments and return them from other functions
  • Use map, filter, and reduce and know exactly what each one returns
  • Spot the forEach-returns-undefined trap and choose the right array method
  • Build configurable behaviour by returning functions (partial application)

Here is the idea that unlocks most of modern JavaScript: functions are values. You can store one in a variable, hand it to another function as an argument, and get one back as a return value — exactly as you would a number or a string.

A higher-order function is simply a function that does one of those two things: it accepts a function as an argument, or it returns a function. You have already met several without the name being attached. map, filter, and reduce are higher-order because they take a callback. multiplyBy(3) is higher-order because it returns a new function. Once you recognise the pattern, an enormous amount of array code stops feeling like magic.

flowchart LR
  IN["input value"] --> H["higher-order<br/>function"]
  H --> OUT["new function"]
  style H fill:#e0900b,color:#fff
A higher-order function can receive a function and return a new one.

Accepting a function: map, filter, reduce

The three array methods you will use every day are all higher-order. Each takes a function and applies it for you, but they differ in what they give back.

map(fn) builds a brand-new array the same length as the original, with fn applied to each element. filter(fn) keeps only the elements for which fn returns something truthy, dropping the rest — the result is a subset, never longer than the input. reduce(fn, seed) folds the whole array down into a single value by carrying an accumulator from one element to the next.

The function you pass in has a narrow job: map's callback transforms one item, filter's callback decides keep-or-drop by returning a boolean, and reduce's callback combines the accumulator with the current item and returns the new accumulator.

map transforms, filter keeps, reduce folds to one value. Press Run.
const nums = [1, 2, 3, 4, 5, 6];
console.log(nums.map(n => n * 10));             // [10,20,30,40,50,60]
console.log(nums.filter(n => n % 2 === 0));      // [2,4,6]
console.log(nums.reduce((sum, n) => sum + n, 0)); // 21
flowchart LR
  A["array"] --> MAP["map(fn)"] --> NA["new array, same length"]
  A --> FILT["filter(fn)"] --> SUB["subset, only matches"]
  A --> RED["reduce(fn, seed)"] --> ONE["single value"]
  style NA fill:#1e7f3d,color:#fff
  style SUB fill:#b45309,color:#fff
  style ONE fill:#3776ab,color:#fff
Same array in, different shape out: a new array, a subset, or one folded value.

Returning a function: closures and partial application

The other half of the pattern is a function that makes functions. multiplyBy(factor) does not multiply anything itself — it builds and returns a new function that remembers factor and multiplies by it later. The returned function has captured factor in a closure, exactly like the makeAdder you saw in the scope lesson.

This is called partial application: you supply some arguments now (the factor) and the rest later (the number to multiply). It is how you turn a general tool into a specific one. multiplyBy(3) gives you a dedicated tripler; multiplyBy(100) gives you a percent converter. One small function, many configured behaviours.

multiplyBy returns a configured function; call it later with the remaining argument.
function multiplyBy(factor) {
  return (n) => n * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5));   // 10
console.log(triple(5));   // 15
flowchart LR
  A["multiplyBy(3)"] -->|"returns"| F["a fn that multiplies by 3"]
  F -->|"call with 4"| R["12"]
  style F fill:#b45309,color:#fff
  style R fill:#1e7f3d,color:#fff
Call once to bake in the factor; call the returned function with the last argument to finish.

The classic trap: forEach returns undefined

Beginners reach for forEach when they mean map, and the bug is silent. forEach runs your callback on each element and then returns undefined — it builds nothing. So this looks reasonable and loses every result:

const result = words.forEach(w => w.toUpperCase());   // result is undefined

The callback dutifully uppercased each word and threw the value away, because forEach ignores what the callback returns. If you want a new array built from the results, you need map. The mnemonic: forEach is for side effects (logging, saving); map is for transforming into a new array. Mixing them up is the most common higher-order bug, and the one you will repair in the exercise below.

forEach builds nothing; map returns the new array you wanted.
const words = ["hi", "yo"];
const r = words.forEach(w => w.toUpperCase());
console.log(r);                 // undefined — forEach returns nothing
const built = words.map(w => w.toUpperCase());
console.log(built);             // ["HI","YO"]
Exercise

What does this print? multiplyBy returns a new function that closes over the factor.

function multiplyBy(factor) {
  return (n) => n * factor;
}
const double = multiplyBy(2);
console.log(double(7));
Exercise

Write compose(f, g) so that compose(f, g)(x) returns f(g(x)). Both f and g are single-argument functions.

function compose(f, g) {
  // return a new function
}
Exercise

This is meant to build and return a new array of uppercased words. It runs without error, but it returns undefined, because it uses forEach — which builds nothing. Repair it by switching to the array method that constructs and returns a new array from each callback's result.

function upperAll(words) {
  const result = words.forEach(w => w.toUpperCase());
  return result;
}
Exercise

Write greaterThan(threshold) that returns a function. The returned function should take a number and return true when that number is greater than threshold. This is the transfer task: like multiplyBy, you return a configured function — but here it produces a boolean you can hand straight to filter.

function greaterThan(threshold) {
  // return a function that compares its argument to threshold
}
Exercise

What does this print? reduce starts the accumulator at the seed (100), then adds each element.

const nums = [1, 2, 3, 4];
console.log(nums.reduce((a, b) => a + b, 100));

Recap

  • A higher-order function accepts a function argument or returns a function (or both).
  • map transforms into a new array, filter keeps a subset, reduce folds to one value — always pass a seed to reduce.
  • forEach returns undefined: use it for side effects, never to build an array. Reach for map when you need the results.
  • A function that returns a function uses a closure to bake in some arguments now and take the rest later (partial application).

Next you'll meet the older pattern JavaScript used for private state before classes — the IIFE and the module pattern — which leans on the very closures and scope rules you have just learned.

Checkpoint quiz

What is a higher-order function?

Why does const r = arr.forEach(fn) leave r as undefined?

If add1(x) = x + 1 and double(x) = x * 2, what does compose(add1, double)(3) return?

Go deeper — technical resources