RizTech Academy logo
RizTech Academy
Functions, Scope and ClosuresLesson 5 of 525 min

Callbacks and higher-order functions

The last lesson kept passing functions to map without explaining what that was. A function is a value like any other — it can be stored, passed and returned. That one fact is behind map, every event listener, and all of asynchronous JavaScript.

A function is a value

function greet() {
  return 'Namaste';
}

console.log(typeof greet);
console.log(typeof greet());
function
string

greet is the function. greet() is the result of running it. One character of difference, and it is the source of a bug you will write within the week:

button.addEventListener('click', handleClick());

That calls handleClick immediately, at the moment the page loads, and hands its return value — probably undefined — to addEventListener as the thing to run on click. So the handler fires once, too early, and then never again.

button.addEventListener('click', handleClick);

Pass the function, do not call it. When you see brackets after a function name in an argument list, stop and check whether you meant them.

Higher-order functions

A function that takes or returns another function. That is the whole definition.

function applyTwice(fn, value) {
  return fn(fn(value));
}

const double = (n) => n * 2;

console.log(applyTwice(double, 3));
12

applyTwice knows nothing about doubling. It knows how to apply something twice, and the caller supplies the something. That is the point: the behaviour becomes a parameter.

A callback, then

A callback is just a function you hand to something else for it to call later. There is nothing special about it — the name describes the role, not a kind of function.

function forEachOrder(orders, action) {
  for (const order of orders) {
    action(order);
  }
}

forEachOrder(['Priya', 'Arjun'], (name) => {
  console.log(`Tiffin for ${name}`);
});
Tiffin for Priya
Tiffin for Arjun

You wrote the loop once and can now do anything per order without touching it. This is exactly what Array.prototype.forEach is, and module 4 gives you it and its better-behaved relatives.

Returning a function

The other half of higher-order, and you have already seen it in closures:

function makeRateCalculator(rate) {
  return (plates) => plates * rate;
}

const standard = makeRateCalculator(80);
const premium = makeRateCalculator(120);

console.log(standard(3));
console.log(premium(3));
240
360

Two calculators built from one recipe, each closing over its own rate. A function factory like this is often clearer than a function taking one more argument, because the rate is decided once, where it is known.

Callbacks that take arguments

The thing calling your callback decides what it passes in. map passes the item, the index and the whole array:

const items = ['dal', 'rice', 'atta'];

items.forEach((item, index) => {
  console.log(`${index + 1}. ${item}`);
});
1. dal
2. rice
3. atta

You may take fewer parameters than you are offered. Extra arguments are simply ignored, which is why items.forEach((item) => ...) is fine.

That tolerance causes one well-known accident:

console.log(['1', '2', '3'].map(Number));
console.log(['1', '2', '3'].map(parseInt));
[ 1, 2, 3 ]
[ 1, NaN, NaN ]

Number takes one argument, so the extra index is ignored. parseInt takes two — the string and the base — so it receives the index as the base: parseInt('2', 1) asks for base 1, which does not exist, giving NaN. Passing a function by name is convenient and occasionally lethal; when in doubt, wrap it: .map((s) => parseInt(s, 10)).

Callbacks that report back

Many callbacks are expected to return something. map builds a new array from whatever you return:

const plates = [3, 5, 2];
const totals = plates.map((count) => count * 80);

console.log(totals);
[ 240, 400, 160 ]

Forget the return and you get an array of undefined:

const broken = plates.map((count) => {
  count * 80;
});

console.log(broken);
[ undefined, undefined, undefined ]

The braces made it a block, so the expression is calculated and thrown away. This is the arrow-function trap from lesson one of this module, in the place you will actually hit it. Either drop the braces or add the return.

Naming them

Short callbacks read well inline. Longer ones deserve a name:

const isDelivered = (order) => order.status === 'delivered';

const delivered = orders.filter(isDelivered);

orders.filter(isDelivered) reads as a sentence, and isDelivered can be tested on its own and reused. If a callback is more than about three lines, or you have written it twice, give it a name.

Where this is going

Every remaining module of this course is built on what you just learnt.

Where The callback is
map, filter, reduce — module 4 What to do with each item
addEventListener — module 7 What to do when it is clicked
setTimeout — module 8 What to do after the wait
.then() — module 8 What to do when the data arrives

The last one is the whole of asynchronous JavaScript. When you write fetch(url).then(showData), you are passing showData to be called later, by something that will not be ready for two hundred milliseconds. If passing functions around feels natural by the end of module 4, module 8 will be much easier than its reputation.

Check your work

typeof greet is 'function' and typeof greet() is 'string'. The brackets run it; without them you have the function itself.

addEventListener('click', handleClick()) is the bug. It runs the handler once at page load and registers its return value — usually undefined — as the listener. Drop the brackets.

applyTwice(double, 3) is 12. Double 3 to 6, double 6 to 12.

makeRateCalculator(80)(3) is 240. The outer call fixes the rate; the inner one supplies the plates.

['1','2','3'].map(Number) is [1, 2, 3]; .map(parseInt) is [1, NaN, NaN]. map passes the index as a second argument, which Number ignores and parseInt takes as the base. parseInt('2', 1) is NaN because base 1 does not exist. parseInt('1', 0) happens to work, because base 0 means "guess".

A map callback with braces and no return gives an array of undefined. Braces make a block; only an expression body returns implicitly.

Practice

  1. Print typeof for a function and for the result of calling it. Make the difference obvious to yourself.
  2. Write applyTwice(fn, value) and pass it a doubler, then a function that appends '!' to a string. One function, two behaviours.
  3. Write forEachOrder(orders, action) and call it twice with different actions — one printing, one counting.
  4. Make the brackets mistake on purpose. Write a function that takes a callback, pass it myFn() instead of myFn, and work out from the error what was actually handed over.
  5. Write makeRateCalculator(rate) and build a standard and a premium calculator from it.
  6. Use forEach with both (item) and (item, index) and confirm taking fewer parameters is fine.
  7. Run the parseInt bug. Map ['1','2','3'] through Number and through parseInt and explain the difference. Then fix it with a wrapper.
  8. Write a map callback with braces and no return, get an array of undefined, then fix it twice — once by removing the braces, once by adding return.
  9. Pull a three-line inline callback out into a named function and decide whether the call site reads better.
  10. Harder. Write makeLogger(prefix) returning a function that prefixes every message, then retry(fn, times) that calls fn until it returns something truthy or the attempts run out, reporting each attempt through a logger you pass in. You will be passing functions into functions that return functions — which is the point.

That is module three, and it is the one that matters most. You now know that scope is decided by where code is written, that closures are what happens when a function outlives that scope, and that this is the single exception — decided by how a function is called. Those three ideas explain most JavaScript behaviour that looks like magic.

Next module: objects and arrays — how data is actually shaped, and the array methods you will use every day. All of them take callbacks, so you have just done the hard part.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship