Callbacks and Higher-Order Functions

Pass functions to other functions, return functions from functions, and understand callbacks, closures and why they power events, timers and array methods.

Lesson 21 of 56Intermediate15 min read

In JavaScript, functions are values. You can store a function in a variable, put it in an array, pass it to another function, and even return it from a function, exactly as you would with a number or a string. Programmers call this having first-class functions, and it's one of the most powerful ideas in the language.

A function that you pass into another function so it can be called later is a callback. You've already used callbacks without the name: the function you give to addEventListener runs when the button is clicked, the one you give to setTimeout runs after the delay, and the one you give to map runs once for each array item. A function that takes or returns other functions is called a higher-order function.

In this lesson you'll write your own callbacks and higher-order functions, learn how closures let a returned function remember variables, and see why deeply nested callbacks led to Promises. These ideas sit underneath almost everything in modern JavaScript, from events to React.

Functions are values

JavaScript
const greet = function (name) { return `Hi ${name}`; };
const shout = (text) => text.toUpperCase();

const tools = [greet, shout];
console.log(tools[1]("hello")); // "HELLO"
  • A function name without brackets, greet, refers to the function itself.
  • With brackets, greet("Sam"), it calls the function and gives you the result.
  • Mixing these up is the most common callback mistake: pass greet, not greet().

Writing a function that takes a callback

JavaScript
function repeat(times, action) {
  for (let i = 1; i <= times; i++) {
    action(i);
  }
}

repeat(3, (n) => console.log("Step", n));

repeat doesn't know or care what action does. It just calls it at the right moment with the right argument. This separation is why callbacks are so useful: the same repeat can log, draw, or save data, depending on the callback you pass. Array methods work the same way: numbers.filter(n => n > 10) calls your function for each item and keeps the ones where it returns true.

Functions that return functions, and closures

JavaScript
function multiplier(factor) {
  return (n) => n * factor;
}

const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5), triple(5)); // 10 15

The returned function remembers factor even after multiplier has finished. That memory is called a closure. Closures let you create configured functions, keep private state such as a counter, and they're the reason event handlers can still use the variables around them long after the setup code has run. You met the idea of where variables live in the scope lesson.

Synchronous and asynchronous callbacks

KindWhen the callback runsExamples
SynchronousImmediately, before the outer function returnsmap, filter, forEach, sort
AsynchronousLater, after something happenssetTimeout, addEventListener, older file and network APIs

When asynchronous steps depend on each other, callbacks get nested inside callbacks, creating a pyramid shape known as callback hell. It's hard to read and harder to handle errors in. Promises and async/await were created to flatten that pyramid.

JavaScript Callbacks Syntax

Callbacks and closures

function run(cb) { cb("done"); }
run(msg => console.log(msg));

const nums = [1, 2, 3].map(n => n * 10);
button.addEventListener("click", handleClick);

function makeCounter() {
  let count = 0;
  return () => ++count;
}

JavaScript Callbacks Summary

  • Functions are values: you can store, pass and return them.
  • A callback is a function passed to another function to be called later.
  • A higher-order function takes or returns functions, like map, filter and addEventListener.
  • Pass the function (fn), not its result (fn()).
  • A closure is a function that remembers variables from where it was created.

JavaScript Callbacks Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Your own higher-order function

function processOrder(order, onSuccess, onError) {
  if (order.items.length === 0) {
    onError("Basket is empty");
    return;
  }
  const total = order.items.reduce((sum, item) => sum + item.price, 0);
  onSuccess(total);
}

const showTotal = (total) => console.log(`Charged £${total.toFixed(2)}`);
const showError = (message) => console.log("Error:", message);

processOrder({ items: [{ price: 4.5 }, { price: 2 }] }, showTotal, showError);
processOrder({ items: [] }, showTotal, showError);
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1The function receives two callbacks, one for success and one for failure.
  2. Line 3On a problem, it calls the error callback.
  3. Line 6reduce itself takes a callback to add up the prices.
  4. Line 7On success, it calls the success callback with the result.
  5. Lines 13 to 14The same function, two outcomes, depending on the data.
Example 2

Closures that remember

function makeCounter(start = 0) {
  let count = start;
  return {
    increment: () => ++count,
    reset: () => (count = start),
  };
}

const likes = makeCounter();
const views = makeCounter(100);

likes.increment();
likes.increment();
views.increment();
console.log("likes:", likes.increment());
console.log("views:", views.increment());
console.log("reset likes:", likes.reset());
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2count is private: nothing outside can change it directly.
  2. Lines 4 to 5Both functions close over the same count.
  3. Lines 9 to 10Each call to makeCounter creates a separate closure with its own count.
  4. Lines 15 to 16Likes reaches 3 and views reaches 102 independently.
Example 3

Callback hell and the way out

function step(name, delay, next) {
  setTimeout(() => { console.log(name); next(); }, delay);
}

step("1. Fetch user", 100, () => {
  step("2. Fetch orders", 100, () => {
    step("3. Fetch invoice", 100, () => {
      console.log("All done, but look at that indentation");
    });
  });
});

const wait = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
  await wait(400);
  console.log("With async/await the same steps read top to bottom");
})();
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 1 to 3A function that does something later, then calls the next step.
  2. Lines 5 to 11Each step nests inside the previous one, creating the pyramid.
  3. Lines 13 to 17Promises and await flatten the same idea into straight lines.

Common JavaScript Callbacks Mistakes

Calling the callback too early

Don't
button.addEventListener("click", save()); // save runs now, not on click
Do
button.addEventListener("click", save);
button.addEventListener("click", () => save("draft"));

Forgetting to return in a callback

Don't
const doubled = [1, 2, 3].map(n => { n * 2 }); // [undefined, undefined, undefined]
Do
const doubled = [1, 2, 3].map(n => n * 2);

With curly braces, an arrow function needs an explicit return.

Losing the remove handle

Don't
el.addEventListener("scroll", () => update());
el.removeEventListener("scroll", () => update()); // a different function, nothing removed
Do
const onScroll = () => update();
el.addEventListener("scroll", onScroll);
el.removeEventListener("scroll", onScroll);

JavaScript Callbacks in Real Projects

Callbacks everywhere

Every event listener in the browser is a callback. Node.js was built around callbacks for file and network access before Promises arrived. React components pass callbacks such as onClick and onChange down to child components. Libraries like Lodash provide higher-order helpers such as debounce, which take your function and return a new, improved one.

JavaScript Callbacks Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Pass logItem as the callback to forEach without calling it.

function logItem(item) { console.log(item); }
["a", "b"].forEach();
Predict the outputmedium

What is logged?

function make(x) {
  return (y) => x + y;
}
const add5 = make(5);
console.log(add5(2), make(1)(1));
Multiple choiceeasy

What is a callback?

Multiple choicemedium

What is a closure?

Fix the bugmedium

applyToAll should return a new array with fn applied to every item, and addTax should be a function returned by withTax that adds 20%. Fix both.

function applyToAll(items, fn) {
  const out = [];
  for (const item of items) out.push(fn);
  return out;
}
function withTax(rate) {
  (price) => price * (1 + rate);
}
const addTax = withTax(0.2);
console.log(applyToAll([10, 20], addTax));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • withTax returns a function
  • Tax is added
  • applyToAll calls the callback for each item

JavaScript Callbacks Quiz

5 questions to check what you learned.

  1. Which is a higher-order function?

    • Math.PI
    • array.map
    • console
    • "text".length
  2. What's wrong with setTimeout(alertUser(), 1000)?

    • Nothing
    • The delay is too short
    • setTimeout needs a string
    • alertUser runs immediately and its result is passed instead
  3. What is callback hell?

    • An error message
    • Deeply nested callbacks that are hard to read and maintain
    • A callback that never runs
    • A browser setting
  4. Does the callback passed to array.map run synchronously?

    • Yes, once per item, before map returns
    • No, it runs later
    • Only for the first item
    • Only in Node.js
  5. Why does each call to makeCounter() keep its own count?

    • Counters are global
    • JavaScript copies the function
    • Because of let
    • Each call creates a new scope, and the returned functions close over that scope

JavaScript Callbacks Challenge

Build a tiny event system

Write createEmitter() that returns an object with:

  1. on(name, callback): stores the callback under the event name (several callbacks per name)
  2. emit(name, data): calls every callback for that name with data, in the order they were added
  3. count(name): returns how many callbacks are stored for that name (0 if none)
function createEmitter() {

}

const shop = createEmitter();
shop.on("sale", item => console.log("Sold:", item));
shop.on("sale", item => console.log("Update stock for", item));
shop.emit("sale", "mug");
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Callbacks run with the data
  • Other events are unaffected
  • count reports stored callbacks