cf.completefrontendCode editorOpen lab
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
    Recognize higher-order functionsTell whether code takes a function, returns a function, or simply calls one.
  • 02
    Swap behavior safelyPass callbacks without accidentally calling them too early, and read callback signatures.
  • 03
    Build clear data pipelinesChoose loops or declarative methods based on the shape and readability of the task.

The big idea

A higher-order function is a function that works with other functions. More precisely, it either takes a function as an argument, returns a function, or both. This is not a special syntax feature. It works because JavaScript treats functions as values: you can store them in variables, put them in objects, pass them around, and call them later.

You have already met the idea in the Callbacks, Arrow functions, Closures, and map, filter & reduce lessons. This lesson ties those pieces together at the start of the Functional programming module.

The short version

A higher-order function lets you separate the repeated shape of work from the behavior plugged into that work.

Real-life analogyA higher-order function is a stand mixer

A stand mixer does not need a different motor for cake batter, bread dough, and whipped cream. You keep the motor and swap the attachment. Higher-order functions give code the same trick: keep the control flow and swap the function that customizes it.

In real life: The stand mixer body
In JavaScript: The higher-order function, such as applyToEach or map
In real life: A whisk, dough hook, or paddle
In JavaScript: The callback function you plug in
In real life: The bowl turning the same way
In JavaScript: The repeated loop or orchestration
In real life: Different food from the same machine
In JavaScript: Different results from the same helper

Where the analogy stops: A mixer can only use attachments made for it. JavaScript will accept any value, so a higher-order helper must be called with a real function or it throws when it tries to call it.

We will move from intuition to precision: first functions as values, then callbacks, then factories that return functions, then the practical difference between imperative loops and declarative pipelines.

Functions as values

A function declaration or function expression creates a value. The value is callable, but it is still a value. That means these two lines do very different jobs:

Pass 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

The first call gives applyToEach the function so the helper can call it once per item. The second line tries to run double immediately, before any item exists. In real code that usually means a missing argument, a surprising result, or a runtime error.

Similar-looking expressions
ExpressionMeaningCommon use
doubleThe function value itselfPass as a callback
double(3)Call the function now with 3Compute a result immediately
() => double(3)A new function that will call laterDelay work until an event or callback

This tiny distinction is the doorway to higher-order functions. Once you can pass behavior around, a helper can say, “I know how to repeat, wait, wrap, sort, or combine. You tell me what to do at the important moment.”

Functions as arguments

INTERACTIVE

A function passed into another function is usually called a callback. The higher-order function decides when to call it and what arguments to provide. The callback decides the custom behavior.

Try one helper named applyToEach. It always loops through the same items, but you can swap in a callback that doubles numbers, labels them, or checks whether they are even.

Callback lab: swap the behavior
One loop, swappable callbackPop 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 exactly the same. The function you pass in decides what each item becomes.

Try it yourself
Callback:

double turns each number into twice itself. The loop does not change; only the callback behavior changes.

The helper receives a function value. If you wrote double(), you would call it immediately and pass the result, not the callback.

This is the same principle behind array methods. map, filter, reduce, sort, and DOM event methods all accept callbacks. They differ in what they pass to the callback and what they do with the return value.

Callback signatures matter

Array callbacks receive more than one argument: typically the element, the index, and the whole array. That is useful when you need it, and surprising when you pass a function that treats the second argument specially.

The classic parseInt trapPop 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)));

The first log prints [1, NaN, NaN]. Why? map calls the callback as callback(element, index, array), and parseInt reads its second argument as the number base. So it runs like parseInt("2", 1) and parseInt("3", 2), which are not useful parses. Wrap it: word => parseInt(word, 10).

Returning functions

STEP THROUGH

Higher-order functions can also create and return functions. These are often called function factories. You call the factory once with settings, and it gives you back a function you can reuse many times.

Real-life analogyReturning functions is a stamp maker

A stamp maker turns one description into a reusable tool. You do not explain “Hi” every time you stamp a card. You make the stamp once, then press it whenever you need the same pattern.

In real life: Describe the stamp once
In JavaScript: Call makeGreeter("Hi") or makeMultiplier(3)
In real life: Receive a physical stamp
In JavaScript: Store the returned function
In real life: Press it many times
In JavaScript: Call the returned function many times
In real life: Each stamp keeps its design
In JavaScript: Each closure keeps its remembered values

Where the analogy stops: A stamp's design is fixed. A returned JavaScript function can remember values that later change too, as makeCounter does.

Step through the factory below. Change the factor, then watch the returned function keep using that factor in later calls. This replay is recorded from the lesson’s real functions; it is a guided trace, not a browser engine debugger.

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

Choose a factory argument, predict both results, then step through the factory call and the later calls to the returned function.

Running in
  1. script
Next: line 7
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function makeMultiplier(factor) {  return function (value) {    return value * factor;  };} multiplier(4);multiplier(10);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the factory argument for makeMultiplier

Changing the factor creates a fresh recorded run. 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.

The closure is the reason the returned function can still read factor. The outer call has finished, but JavaScript keeps the lexical environment alive because the returned function needs it. The Closures and Closure patterns lessons go deeper into that memory model.

Factory tools: multiplier, counter, once, wrapper
Four higher-order patternsPop 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) creates a reusable “times 3” function.
  • makeCounter() creates private state without a class.
  • once(fn) protects work that should happen one time.
  • withLogging(fn) adds behavior around an existing function.
Try it yourself

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

These are ordinary functions. The advanced part is treating functions as configurable values.

Declarative vs imperative

COMPARE

Higher-order functions often make code more declarative. Imperative code describes every mechanical step. Declarative code names the desired transformations and lets trusted helpers handle the repetition.

Real-life analogyDeclarative is ordering a pizza

At a restaurant you usually describe the result you want. You do not explain dough hydration, oven temperature, and cheese placement unless you are training the chef. Declarative code is similar: it communicates intent first.

In real life: Order “large pizza, no onions”
In JavaScript: Say orders.filter(...).reduce(...)
In real life: Give the cook every chopping and baking step
In JavaScript: Write every loop, branch, push, and addition
In real life: The kitchen still performs the steps
In JavaScript: The array methods still loop internally

Where the analogy stops: Sometimes you really are the cook. If the steps are unusual, performance-sensitive, or need early exits, an explicit loop can be clearer.

Compare the same small report two ways: keep paid orders, format labels, and total their prices. The counted visits are deliberately visible so you do not confuse “more declarative” with “always fewer operations.”

Imperative vs declarative report
Declarative 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 simple loop can still be clearer for complex branching, early exits, or heavy performance-sensitive work.

Try it yourself
Read it as:

The pipeline names the intent: keep paid orders, format labels, reduce to a total. It does more callback visits here, but the business rule is easy to scan.

Both versions compute the same report. Declarative is not automatically faster; it is often easier to read when the operations line up with the problem.

Use the version your teammate can safely change next month. For a straight “keep, transform, combine” flow, a pipeline reads like the business rule. For nested branches, breaking early, or mutating a shared resource, a loop may say the truth more clearly.

map, filter & reduce revisited

EDGE CASES

The earlier array lesson introduced the job of each method. Now read them as higher-order functions. map takes a callback and returns a new array. filter takes a predicate callback and returns the items that pass. reduce takes a reducer callback and returns whatever the accumulator becomes.

reduce can build objects, 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));

The first reducer groups orders into an object by status. The next two show that reduce is powerful enough to implement map and filter: start with an empty array, push the right results, and return the accumulator. That is useful to understand, but usually not the clearest production code. Prefer the specific method when the specific method says the job.

The reduce pitfall professionals still check

Without an initial value, reduce uses the first array element as the accumulator. On an empty array there is no first element, so [].reduce((a, b) => a + b) throws a TypeError. Provide an initial value when the array might be empty.

Also remember callback signatures. Array callbacks receive element, index, and array. That is why arr.map(parseInt) surprises and why a sorter callback has its own shape: (a, b) => number. Not every callback API passes the same arguments.

Where you’ll use this

Higher-order functions are not just a functional-programming exercise. They show up in everyday frontend work:

Realistic higher-order signaturesJavaScript
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)}/>;
  • Event handlers store callbacks for later: the browser calls handleSave when the click happens.
  • Array pipelines filter and map API data into display data.
  • Middleware and wrappers add logging, authentication, timing, or retries around a function.
  • React-style components often receive callbacks like onSelect so a child can report a user action without knowing what the parent will do.
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 expression by whether it 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 definition is small: takes a function or returns one. The complexity comes from unclear callback names, hidden side effects, or trying to be clever when a loop would be clearer.

“Callbacks and higher-order functions are the same thing.”

A callback is the function being passed. The higher-order function is the function receiving it, returning it, or wrapping it.

“You always write fewer operations with pipelines.”

A chain can visit arrays multiple times. That is often fine for readability, but a single loop can be more direct for hot paths.

“reduce is the advanced replacement for everything.”

reduce can build almost anything, but specific methods communicate intent better. Use map for one output per input and filter for keep-or-drop tests.

“A returned function shares settings with every factory call.”

Each factory call gets its own remembered values. Assigning the same returned function to another variable shares state; calling the factory again creates a fresh closure.

Do not mix these up
IdeaFocusTypical codeWatch out for
CallbackA function passed in to be called later or repeatedlyitems.map(fn)Pass fn, not fn()
Function factoryA function that creates a customized functionmakeMultiplier(3)Each call can remember separate values
WrapperA function around another functionwithLogging(save)Return the original result unless you mean to change it
Declarative pipelineName the transformations, not every loop steporders.filter(...).map(...)Too many tiny steps can hide simple logic

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a custom callback

Read the helper and predict the exact array printed.

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 the factory argument and call argument 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 reason about code that should run once, such as sending a receipt or initializing a widget.

      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 · ChallengeBuild a paid-order total

        Explain why the pipeline reads as “keep paid orders, then total them.” 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 an empty reduce

          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

            Answer once from your prediction, then read every explanation. The wrong answers are chosen to catch real bugs.

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

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the callback lab print for doubled numbers?

              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 8Why is applyToEach(numbers, double) different from applyToEach(numbers, double())?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does the multiplier factory 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 reduce has no initial value on an empty array?

              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?

              Choose an answer to see the explanation.

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

              Choose an answer to see the explanation.

            Key takeaways

            • A higher-order function takes a function as an argument, returns a function, or both.
            • Pass the function value, such as fn, when an API needs to call it later; fn() calls now.
            • Returned functions can remember factory settings through closures.
            • Declarative pipelines can make intent clearer, but loops are still valid when they are clearer or more efficient.
            • Array callback signatures matter: map passes element, index, and array, which explains map(parseInt).
            • Always consider an initial value for reduce, especially when an array might be empty.

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

            Up next: Pure functions & side effects, where you will separate calculations that always give the same result from code that changes the outside world.

            CompleteFrontend Clear concepts. Working examples.