Callbacks & Higher-Order Functions

A callback is a function you hand to another function so it can call it later. A higher-order function is one that takes functions as arguments or returns them. This idea — passing behaviour around like data — is at the heart of array methods, event handling and asynchronous JavaScript.

A callback, step by step

JS callback.js
function repeat(times, action) {
  for (let i = 1; i <= times; i++) {
    action(i);
  }
}

repeat(3, n => console.log(`Step ${n}`));
Output
Step 1
Step 2
Step 3

repeat knows how many times to do something; the callback decides what to do. Separating those two concerns is what makes higher-order functions so reusable.

Callbacks you already know

APIThe callback is called…
array.forEach(fn), map, filter…Once for each element
array.sort((a, b) => …)To compare pairs of elements
element.addEventListener("click", fn)Each time the event happens
setTimeout(fn, ms)Once, after a delay
promise.then(fn)When an asynchronous result arrives

Synchronous vs asynchronous callbacks

Some callbacks run immediately, before the higher-order function returns (map, forEach). Others run later (setTimeout, events, network responses):

JS sync-async.js
console.log("1. start");

[1, 2].forEach(n => console.log(`2. sync callback ${n}`));

setTimeout(() => console.log("4. async callback"), 0);

console.log("3. end");
Output
1. start
2. sync callback 1
2. sync callback 2
3. end
4. async callback

Even with a delay of 0, the timer callback waits until the current code has finished. Lesson 51 explains why, with the event loop.

Writing your own higher-order functions

Here's a simplified version of filter, to show there's no magic involved:

JS my-filter.js
function keep(items, test) {
  const result = [];
  for (const item of items) {
    if (test(item)) result.push(item);
  }
  return result;
}

const words = ["sun", "moonlight", "sky", "starfield"];
console.log(keep(words, w => w.length > 3));
console.log(keep([5, 12, 8, 21], n => n % 2 === 0));
Output
[ 'moonlight', 'starfield' ]
[ 12, 8 ]

Functions that return functions

Higher-order functions can also produce new functions. These small helpers combine with array methods nicely:

JS returning.js
const greaterThan = min => n => n > min;
const property = key => obj => obj[key];

const scores = [45, 82, 67, 91];
console.log(scores.filter(greaterThan(60)));

const people = [{ name: "Ida" }, { name: "Joe" }];
console.log(people.map(property("name")));
Output
[ 82, 67, 91 ]
[ 'Ida', 'Joe' ]

Composing functions

Composition chains small functions into a pipeline, where each function's output feeds the next:

JS compose.js
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input);

const trim = s => s.trim();
const lower = s => s.toLowerCase();
const dashes = s => s.replace(/\s+/g, "-");

const slugify = pipe(trim, lower, dashes);
console.log(slugify("  Hello Big World "));
Output
hello-big-world

Callback pitfalls

Calling instead of passing

JS pitfall-call.js
function sayDone() {
  return "done";
}

setTimeout(sayDone, 0);
const wrong = sayDone();
console.log(typeof wrong);
Output
string

setTimeout(sayDone(), 0) would call sayDone immediately and pass its result (a string) as the "callback". Pass the function itself, without parentheses.

Unexpected extra arguments

JS pitfall-args.js
console.log(["1", "2", "3"].map(parseInt));
console.log(["1", "2", "3"].map(s => parseInt(s, 10)));
Output
[ 1, NaN, NaN ]
[ 1, 2, 3 ]

map calls its callback with (item, index, array). parseInt treats the second argument as the number base, so parseInt("2", 1) is NaN. Wrapping in an arrow function controls exactly which arguments are passed.

Callback hell

Before promises, a sequence of asynchronous steps meant callbacks nested inside callbacks inside callbacks — a "pyramid of doom" that was hard to read and harder to handle errors in. Promises and async/await (lessons 52–53) were designed to fix that.

Try it yourself

Try it yourself
Console output