completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Higher-order functions

Use functions as values: pass callbacks into helpers, return customized functions from factories, compare declarative array pipelines with loops, and revisit map, filter, and reduce with professional edge cases.

By the end, you can
  • 01
    Spot higher-order functionsTell whether code takes a function, returns a function, or just calls one.
  • 02
    Pass functions safelyGive a helper the function itself, not its result, and check which arguments the helper gives your function.
  • 03
    Choose a loop or a pipelineDecide between a plain loop and array methods like filter and map by which one is easier to read.

The big idea

A higher-order function is a function that works with other functions. It does at least one of these:

  • It takes a function as an argument. An argument is a value you give a function when you call it.
  • It returns a function as its result.

Some do both. There is no special syntax for this. It works because in JavaScript a function is a value, like a number or a string. You can keep a function in a variable, put it in an object, give it to another function, and call it later.

You have already used this idea in the Callbacks, Arrow functions, Closures, and map, filter & reduce lessons. This lesson puts those pieces together. It is the first lesson of the Functional programming module.

The short version

A higher-order function splits a job into two parts. The helper does the part that is always the same, such as going through every item. The function you give it does the part that changes, such as what to do with each item.

Real-life analogyA stand mixer with swappable tools

A kitchen stand mixer has one motor. To whip cream you clip on a whisk. To knead bread you clip on a dough hook. You don't buy a new mixer for each food. You keep the motor and swap the tool.

In real life: The mixer's motor and stand
In JavaScript: The higher-order function, such as applyToEach or map
In real life: The whisk, dough hook, or paddle you clip on
In JavaScript: The function you pass in
In real life: The motor turns the same way every time
In JavaScript: The loop that is always the same
In real life: Cream, dough, or batter from one machine
In JavaScript: Different results from one helper

The lesson goes in this order:

  1. Functions are values.
  2. Passing a function into another function.
  3. Getting a function back from another function.
  4. Writing every step in a loop, or naming the jobs with array methods.
  5. A second look at map, filter, and reduce.

Functions as values

When you write a function, you make a value. You can call that value with round brackets, but it is still a value. So these two lines do very different jobs:

Give the function, not its resultPop out in the code editor (opens in a new tab)JavaScript
const double = n => n * 2; applyToEach([1, 2, 3], double);  // pass the function valueapplyToEach([1, 2, 3], double()); // call now, then pass the result

Line 1 makes a function called double. It takes a number n and returns n * 2.

Line 3 gives applyToEach the function itself. There are no call brackets after double. The helper keeps the function and calls it once for each item.

Line 4 has call brackets: double(). So double runs right away, before the helper starts, with no number. n is undefined, and undefined * 2 is NaN ("not a number"). The helper gets NaN. When it tries to call that, JavaScript throws a TypeError, because NaN is not a function.

Left: applyToEach with double. The box fn holds the double function, so the helper can call it. Right: applyToEach with double(). double runs at once with no number and gives NaN, so the box fn holds NaN, and calling it throws a TypeError.applyToEach(…, double)gets the functiondoublefncan call itapplyToEach(…, double())gets the result: NaNNaNfnTypeError
Without call brackets, the helper gets the function itself. With double(), it gets the result of a call that already ran, so it has nothing to call.

Code like this often looks almost the same but means something different. The table below shows three of them. Read each row as: "when I write this, it means this, and I use it for this."

Three ways to write double that look alike
When you writeWhat it meansWhen you use it
doubleThe function itself. Nothing runs yet.You give it to a helper that will call it.
double(3)Call the function now with 3. You get 6.You want the answer right now.
() => double(3)A new small function. When someone calls it, it calls double(3).You want double(3) to run later, for example after a click.

This small difference is the key to the whole lesson. Once you can hand a function to a helper, the helper can say: "I know how to repeat, wait, sort, or combine. You tell me what to do at each step."

Functions as arguments

INTERACTIVE

A function that you pass into another function is called a callback. The two functions share the work:

  • The higher-order function decides when to call the callback, and what to give it.
  • The callback decides what happens each time it is called.

Try a helper called applyToEach. It takes a list of items and a callback. It goes through the items with a for...of loop, which runs once for each item. For each item it calls the callback and keeps what the callback returns. You can swap in a callback that doubles each number, turns it into a label, or checks if it is even.

The items 1, 2, 3 go into a box called applyToEach. A double function card sits in the box as the callback. Out come 2, 4, 6.[1, 2, 3]itemscallbackdoubleapplyToEachsame loop[2, 4, 6]result
applyToEach runs the same loop every time. The callback you pass in, here double, decides what each item becomes.
Callback lab: swap the callback
One loop, a callback you can swapPop out in the code editor (opens in a new tab)JavaScript
function applyToEach(items, fn) {  const results = [];  for (const item of items) {    results.push(fn(item));  }  return results;} const numbers = [1, 2, 3];applyToEach(numbers, double);applyToEach(numbers, label);applyToEach(numbers, isEven);
Real resultdouble
items[1, 2, 3]
fndouble
result[2,4,6]

The for...of loop stays the same. The function you pass in decides what each item becomes.

Try it yourself
Callback:

double turns each number into twice the number. The loop stays the same; only the callback changes.

The helper gets the function itself. If you wrote double(), the function would run right away, and the helper would get its result instead.

The array methods you know work the same way. map, filter, reduce, and sort all take a callback. So does addEventListener, which runs your function when something happens on the page, such as a click. These methods differ in two ways:

  • What they give to the callback.
  • What they do with the value the callback returns.
Check what a callback receives

Array methods give their callback more than one argument. Usually they give three: the item, its position (the index, starting at 0), and the whole array. Your callback can ignore the ones it doesn't need. But if your callback uses its second argument for something else, it will get the index there anyway.

parseInt gets the index as a second argumentPop out in the code editor (opens in a new tab)JavaScript
const words = ["1", "2", "3"];console.log(words.map(parseInt));console.log(words.map(word => parseInt(word, 10)));

parseInt turns text into a whole number. It takes an optional second argument: the number base, such as 10 for normal numbers or 2 for binary.

Line 2 prints [1, NaN, NaN]. Here is why. map calls its callback as callback(item, index, array). So parseInt gets the index as its number base:

  • parseInt("1", 0) gives 1. A base of 0 means "work it out", so it uses base 10.
  • parseInt("2", 1) gives NaN. Base 1 doesn't exist.
  • parseInt("3", 2) gives NaN. Base 2 only has the digits 0 and 1.

Line 3 fixes it with a small arrow function: word => parseInt(word, 10). Now parseInt only gets the word and the base 10, and it prints [1, 2, 3].

A higher-order function can also take a function and return a new one that does a little more. That new function is called a wrapper. It adds some work before or after the original function, like a gift wrap around a present. The present inside is unchanged.

A wrapper must pass the call throughPop out in the code editor (opens in a new tab)JavaScript
const cart = {
  taxRate: 0.1,
  total(price, quantity) {
    return price * quantity * (1 + this.taxRate);
  }
};

function withLogging(fn) {
  return function (...args) {
    console.log("args", args.join(", "));
    const result = fn.apply(this, args);
    console.log("result", result);
    return result;
  };
}

cart.total = withLogging(cart.total);
console.log(cart.total(10, 2));

Here is the code, step by step:

  1. Lines 1 to 6 make a cart with a tax rate of 0.1 and a total method. Inside the method, this means the cart, so this.taxRate is 0.1.
  2. Line 9: ...args collects every argument the wrapper gets into one array, called args.
  3. Line 10 prints the arguments.
  4. Line 11 calls the original function with fn.apply(this, args). apply calls fn with the same this (the cart) and the same arguments.
  5. Lines 12 and 13 print the result and return it.
  6. Line 17 replaces cart.total with the wrapped version. Line 18 calls it.

It prints args 10, 2, then result 22, then 22. The original method still reads the cart's tax rate (10 × 2 × 1.1 = 22), and the wrapper returns its result unchanged.

A wrapper that prints before and afterPop out in the code editor (opens in a new tab)JavaScript
function withTiming(fn, label) {
  return (...args) => {
    console.log(label + " start");
    const result = fn(...args);
    console.log(label + " end");
    return result;
  };
}

const saveProfile = withTiming(
  name => "saved " + name,
  "saveProfile",
);
console.log(saveProfile("Asha"));

withTiming prints saveProfile start, calls the original function, prints saveProfile end, and returns the result. So the last line prints saved Asha.

A real timing wrapper reads performance.now() before and after the call. That method returns the current time in milliseconds, so the wrapper can print how long the call took. Every wrapper follows the same rules:

  • Add your work before or after the call.
  • Call the original function once.
  • Pass on all its arguments.
  • Return its result.

If a wrapper forgets the last rule, every call through it returns undefined, and no error tells you why.

Returning functions

STEP THROUGH

A higher-order function can also make a new function and return it. A function that does this is called a function factory. You call the factory once with a setting. It gives you back a function that you can call many times.

Real-life analogyOrdering a custom rubber stamp

You order a rubber stamp that says "Hi". You tell the shop what to write only once. After that, you press the stamp on every card. You don't explain "Hi" again each time.

In real life: Tell the shop what the stamp should say, once
In JavaScript: Call makeGreeter("Hi") or makeMultiplier(3)
In real life: Get a stamp back
In JavaScript: Keep the returned function in a variable
In real life: Press it many times
In JavaScript: Call the returned function many times
In real life: Each stamp keeps its own design
In JavaScript: Each returned function keeps its own setting

Step through the factory below. Pick a factor, then watch the returned function keep using that factor in later calls. The steps were recorded from the lesson's real functions. It is a guided replay, not the browser's own debugger.

Step through a function factory
Step 0 of 9Ready
Your turn: follow the blue line

Pick a number for the factory and predict both results. Then step through the factory call and the two later calls to the returned function.

Running in
  1. script
Next: line 7
function makeMultiplier(factor) {  return function (value) {    return value * factor;  };} multiplier(4);multiplier(10);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the number to give makeMultiplier

Each choice starts a new replay. Predict multiplier(4) and multiplier(10) first.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
makeMultiplier(3) is called once. It returns a function, kept in the box multiplier, that remembers factor is 3. Later, multiplier(4) gives 12 and multiplier(10) gives 30.call oncemakeMultiplier(3)value * factormultiplierremembers factor = 3multiplier(4) → 12multiplier(10) → 30call it later
Call the factory once with 3. It hands back a new function that remembers factor is 3, and you can call that function as often as you like.

How can the returned function still read factor after makeMultiplier has finished? It is a closure: a function that remembers the variables around the place where it was made. JavaScript keeps factor alive because the returned function still needs it. The Closures and Closure patterns lessons explain how that memory works.

The block below shows four common functions that return functions:

Four functions that return functions
Four functions that return functionsPop out in the code editor (opens in a new tab)JavaScript
function makeMultiplier(n) {  return value => value * n;} function makeCounter() {  let count = 0;  return () => {    count += 1;    return count;  };} function once(fn) {  let called = false;  let result;  return (...args) => {    if (!called) {      called = true;      result = fn(...args);    }    return result;  };} function withLogging(fn) {  return value => {    console.log("calling with", value);    return fn(value);  };}
What to noticefactories
  • makeMultiplier(3) makes a function that multiplies by 3, ready to use again and again.
  • makeCounter() makes a counter whose count no other code can touch.
  • once(fn) makes a function that runs fn only the first time.
  • withLogging(fn) makes a function that prints, then calls fn.
Try it yourself

Each returned function keeps the values from the call that made it: the multiplier's n, the counter's count, and once's called and result.

These are ordinary functions. The only new idea is that a function can make and return another function.

Declarative vs imperative

COMPARE

There are two ways to write the same job:

  • Imperative code spells out every step: make a list, go through each item, check it, add it.
  • Declarative code names the jobs you want done, such as "keep the paid orders", and lets a helper like filter do the steps.

Higher-order functions often make code more declarative. A chain of array methods, where each method passes its result to the next, is called a pipeline.

Real-life analogyOrdering a pizza

At a restaurant you say what you want: "A large pizza, no onions." You don't tell the cook how to knead the dough or how hot the oven should be. The cook still does all those steps. You just don't have to write them out.

In real life: Say: large pizza, no onions
In JavaScript: Write orders.filter(...).reduce(...)
In real life: Tell the cook every chopping and baking step
In JavaScript: Write every loop, if, push, and addition
In real life: The kitchen still does every step
In JavaScript: The array methods still loop inside

Below, the same small report is written both ways. Both keep the paid orders, make a label for each, and add up their totals. The lab also counts how many times each version visits an order. That count shows that the declarative version doesn't always do less work.

The same report: loop or pipeline
The report as an array pipelinePop out in the code editor (opens in a new tab)JavaScript
const paid = orders.filter(order => order.paid);const paidLabels = paid.map(order => order.id + ": $" + order.total);const total = paid.reduce((sum, order) => sum + order.total, 0);const steps = orders.length + paid.length + paid.length; console.log(paidLabels);console.log(total);console.log(steps);
Same output7 visits
paidLabels["A100: $18","C300: $24"]
total42
counted visits7

A plain loop can still be clearer when there are many branches, when you stop early, or when the code runs very often.

Try it yourself
Read it as:

The pipeline names the jobs: keep the paid orders, make labels, add up a total. Here it visits orders more times, but the rule is easy to read.

Both versions give the same report. A pipeline is not always faster. It is often easier to read when its steps match the job.
Left: a for loop window lists every step: visit an order, check if paid, push a label, add to the total. Right: a pipeline of three function cards, filter, then map, then reduce. Both give total 42.loop: every stepfor loop1. visit an order2. check if paid3. push a label4. add to the totaltotal: 42pipeline: name the jobsfiltermapreducetotal: 42
The loop spells out every step. The pipeline names three jobs: keep the paid orders, make labels, add up the totals. Both print the same total, 42.

Sometimes a plain loop is the clearer choice. This loop looks for the first price above 5:

A loop that stops earlyPop out in the code editor (opens in a new tab)JavaScript
const prices = [4, 7, 6];
let firstExpensive;

for (const price of prices) {
  if (price > 5) {
    firstExpensive = price;
    break;
  }
}

console.log(firstExpensive);

break stops a loop at once. The loop finds 7, keeps it, and stops, so it never looks at 6. It prints 7.

You could write prices.filter(price => price > 5)[0] instead. It gives 7 too. But it checks every price and builds a new array first, and the code doesn't show that you only wanted the first match.

Choose a loop when you need:

  • to stop early with break,
  • several variables that change as you go,
  • many branches of if and else, or
  • code that runs very often, where extra arrays slow things down.

Choose map, filter, and reduce when their names make the job easier to read.

In the end, pick the version a teammate can safely change next month. For "keep, change, combine", a pipeline reads like the rule itself. For many branches or stopping early, a loop shows what really happens.

map, filter & reduce revisited

EDGE CASES

The earlier array lesson showed what each of these methods does. Now look at them as higher-order functions. Each one takes a callback:

  • map calls its callback for each item and returns a new array of the results.
  • filter takes a callback that returns true or false. It returns a new array of the items where the callback returned true.
  • reduce takes a callback that combines items one by one into a single result. The result so far is called the accumulator. reduce returns the final accumulator.
reduce can build an object, and copy map and filterPop out in the code editor (opens in a new tab)JavaScript
const orders = [  { status: "paid", total: 18 },  { status: "draft", total: 9 },  { status: "paid", total: 24 }]; const grouped = orders.reduce((groups, order) => {  groups[order.status] = (groups[order.status] ?? 0) + 1;  return groups;}, {}); const mapped = [1, 2, 3].reduce((result, n) => {  result.push(n * 2);  return result;}, []); const filtered = [1, 2, 3, 4].reduce((result, n) => {  if (n % 2 === 0) result.push(n);  return result;}, []); console.log(grouped);console.log(mapped);console.log(filtered);console.log([].reduce((a, b) => a + b));

Here is what each part does:

  1. Lines 7 to 10 count the orders by status. The accumulator groups starts as an empty object, {}. ?? means "use 0 if there is no count yet". Line 22 prints { paid: 2, draft: 1 }.
  2. Lines 12 to 15 copy what map does. They start with an empty array and push each number doubled. Line 23 prints [2, 4, 6].
  3. Lines 17 to 20 copy what filter does. They push only the even numbers. Line 24 prints [2, 4].
  4. Line 25 calls reduce on an empty array with no starting value. It throws an error.

So reduce can do the job of map and filter. That is good to know, but it is harder to read. When map or filter already says what you mean, use it.

reduce on an empty array

If you give reduce no starting value, it uses the first item as the starting accumulator. An empty array has no first item. So [].reduce((a, b) => a + b) throws a TypeError. When the array might be empty, pass a starting value as the second argument, such as 0.

Remember what each method gives its callback. Array callbacks get the item, the index, and the array. That is why arr.map(parseInt) goes wrong. Other methods give something else. The callback for sort gets two items, (a, b), and returns a number that says which comes first.

Where you’ll use this

You will meet higher-order functions in everyday frontend code:

Higher-order functions in real codeJavaScript
button.addEventListener("click", handleSave); const visibleNames = users  .filter(user => user.active)  .map(user => user.name); app.use(withLogging(authMiddleware)); <ProductList  products={products}  onSelect={product => setSelected(product.id)}/>;
  • Line 1: an event handler. The browser keeps handleSave and calls it when the click happens.
  • Lines 3 to 5: an array pipeline. It keeps the active users, then takes their names for the page.
  • Line 7: a wrapper. On a server, middleware is a function that runs before the code that answers a request. Wrappers like withLogging add logging, login checks, timing, or retries around it.
  • Lines 9 to 12: a React-style component. It gets a callback, onSelect, from its parent. When the user picks a product, the component calls onSelect. It doesn't need to know what the parent does next.
Higher-order or not?
  • numbers.map(n => n * 2)
  • makeMultiplier(3)
  • withLogging(saveOrder)
  • Number('42')
  • button.addEventListener('click', handleClick)
  • once(sendReceipt)
  • items.sort((a, b) => a.score - b.score)
  • names.join(', ')
Try it yourself
0 of 8 correct

Sort each line by what it does: takes a function, returns a function, does both, or does neither.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

"Higher-order means complicated."

The rule is small: it takes a function, or it returns one. Code gets hard to read for other reasons: unclear callback names, callbacks that secretly change things, or a clever chain where a plain loop would be clearer.

"A callback and a higher-order function are the same thing."

The callback is the function you pass in. The higher-order function is the one that takes it, returns it, or wraps it.

"A pipeline always does less work."

A chain can go through the items several times, once per method. That is usually fine. But for code that runs very often, one loop can be faster.

"reduce is the advanced choice for everything."

reduce can build almost anything, but other methods say what you mean more clearly. Use map to get one new item for each item. Use filter to keep some items and drop others.

"Every function from one factory shares the same settings."

Each call to the factory makes a new function with its own remembered values. If you copy that function into another variable, both names use the same function and the same values. But calling the factory again makes a fresh function with fresh values.

The table below keeps four similar ideas apart. Read each row as: "this idea is this, it looks like this in code, and this is the mistake to avoid."

Four ideas that are easy to mix up
Which ideaWhat it isWhat it looks likeWhat to watch out for
CallbackA function you pass in, so a helper can call it later or many times.items.map(fn)Pass fn, not fn().
Function factoryA function that makes and returns a new function.makeMultiplier(3)Each call makes a new function with its own remembered values.
WrapperA function that takes a function and returns a new one that calls it, with extra work around it.withLogging(save)Return the original result, unless you really mean to change it.
PipelineA chain of array methods that names each job instead of writing every loop step.orders.filter(...).map(...)Too many tiny steps can make simple code hard to follow.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict what a callback returns

Read the helper and predict the exact array it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function applyToEach(items, fn) {
  const results = [];
  for (const item of items) results.push(fn(item));
  return results;
}
console.log(applyToEach([2, 4], n => n + 1));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeWrite a multiplier factory

    Paste the program into your console. Then change both numbers to make your own multiplier.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function makeMultiplier(n) {
      return value => value * n;
    }
    const times4 = makeMultiplier(4);
    console.log(times4(6));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug: too many sends

      Use the wrapper to check code that must run only once, such as sending a receipt or setting up a widget. What are the three printed lines?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function once(fn) {
        let called = false;
        let result;
        return (...args) => {
          if (!called) {
            called = true;
            result = fn(...args);
          }
          return result;
        };
      }
      let sends = 0;
      const sendOnce = once(() => {
        sends += 1;
        return sends;
      });
      console.log(sendOnce());
      console.log(sendOnce());
      console.log(sends);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeAdd up the paid orders

        Say why the pipeline reads as "keep the paid orders, then add them up". Then check the printed number.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const orders = [
          { paid: true, total: 5 },
          { paid: false, total: 20 },
          { paid: true, total: 7 }
        ];
        const total = orders
          .filter(order => order.paid)
          .reduce((sum, order) => sum + order.total, 0);
        console.log(total);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix reduce on an empty array

          Write the safe version and type what it prints.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          // Buggy when the array is empty:
          [].reduce((sum, n) => sum + n);
          
          // Fix it by adding an initial value.

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            Pick your answer first, then read every explanation. The wrong answers are mistakes people really make.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which sentence says what a higher-order function is?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function applyToEach(items, fn) {
                const results = [];
                for (const item of items) results.push(fn(item));
                return results;
              }
              console.log(applyToEach([1, 2, 3], n => n * 2));

              Choose an answer to see the explanation.

            3. Question 3 of 8How is applyToEach(numbers, double) different from applyToEach(numbers, double())?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this factory code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeMultiplier(n) {
                return value => value * n;
              }
              const triple = makeMultiplier(3);
              console.log(triple(5));

              Choose an answer to see the explanation.

            5. Question 5 of 8What does ['1', '2', '3'].map(parseInt) print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log(["1", "2", "3"].map(parseInt));

              Choose an answer to see the explanation.

            6. Question 6 of 8What happens when you call reduce on an empty array with no starting value?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              try {
                [].reduce((sum, n) => sum + n);
              } catch (error) {
                console.log(error.name);
              }

              Choose an answer to see the explanation.

            7. Question 7 of 8Which version is more declarative, that is, names the jobs instead of spelling out every step?

              Choose an answer to see the explanation.

            8. Question 8 of 8withLogging(fn) returns a new function that calls fn. What kind of higher-order function is it?

              Choose an answer to see the explanation.

            Key takeaways

            • A higher-order function takes a function as an argument, returns a function, or does both.
            • Pass fn when a helper should call it later. fn() calls it now.
            • A returned function remembers the factory's settings. That memory is a closure.
            • A pipeline can make the jobs easy to read. A loop is still fine when it is clearer or faster.
            • Array callbacks get the item, the index, and the array. That is why map(parseInt) goes wrong.
            • Give reduce a starting value, especially when the array might be empty.

            One-line definition.
            A higher-order function is a function that takes another function, returns another function, or both.

            Up next: Pure functions & side effects. You will learn to tell apart functions that always give the same result for the same input from code that changes things outside itself.

            CompleteFrontend Clear concepts. Working examples.