Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~25 min· topic 7 of 9

Topic 3.7

Callbacks and Higher-Order Functions

In one line

In JavaScript, functions are values: you can store them, pass them to other functions (callbacks) and return them; a higher-order function is one that takes or returns a function, like map, filter, addEventListener and setTimeout.

Think of it like this

Leaving your phone number with a shop. You say: "when the shoes in my size arrive, call me back". You hand over *how to reach you* (a callback), and the shop decides *when* to use it. The shop is like a higher-order function: its job involves calling a function someone else gave it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

First-class function
A function that can be used like any other value: stored, passed and returned.
Callback
A function you pass to another function so it can call it later or for each item.
Higher-order function
A function that takes a function as input, returns a function, or both.
Synchronous
Runs now, in order, before the next line continues.
Asynchronous
Runs later, after the current code has finished, for example when a timer fires or a click happens.
Radix
The number base, like 10 for normal numbers or 2 for binary. parseInt's second parameter.
Wrapper / decorator
A function that takes a function and returns a new one with extra behaviour around it.
Declarative
Code that says what result you want (filter(isCheap)), not every step of how to get it.

Step by step

01Store a function in a variable

A function name without () is just a value. You can give it another name and call it through that name.

values.jswhole filejavascript
function shout(text) {
  return text.toUpperCase() + "!";
}
const yell = shout;      // no () : the function itself
console.log(yell("hi")); // "HI!"

02Pass it to another function

speak doesn't know how to style text. It takes a styling function and calls it. Swap the callback and you swap the behaviour.

callback.jswhole filejavascript
function speak(text, style) {
  return style(text); // call the callback
}
speak("Hello", shout);            // "HELLO!"
speak("Hello", t => t + "?");     // "Hello?"

03Use the built-in higher-order functions

map, filter and reduce take a callback and do the looping for you.

arrays.jswhole filejavascript
const prices = [120, 45, 300, 80];
prices.map(p => p * 2);             // [240, 90, 600, 160]
prices.filter(p => p < 100);        // [45, 80]
prices.reduce((sum, p) => sum + p, 0); // 545

04Know when the callback runs

Array callbacks run immediately. Timer and event callbacks run later, after the current script finishes.

Know when the callback runsdiagram
Rendering diagram…

05Return a function

A wrapper takes a function and returns a new function that adds something around each call.

wrap.jswhole filejavascript
function logged(fn) {
  return function (...args) {
    console.log("calling with", args);
    return fn(...args);
  };
}
const loggedAdd = logged((a, b) => a + b);
loggedAdd(2, 3); // logs: calling with [ 2, 3 ], returns 5

Try it yourself

  1. 1

    Write your own style

    In the first example, write a robot function that puts a space between every letter (hint: text.split("").join(" ")) and pass it to speak.

  2. 2

    Chain them

    In the second example, write one line that doubles every price, keeps only results under 200, then adds them up. Predict the number first.

  3. 3

    Reorder the output

    In the third example, change the timer's delay from 0 to 1000 and set nothing else. Does the order change? Why not?

Code & diagrams

Functions are values New tab

Console

(nothing was printed)

Expected console output

HELLO!
hello...
Hello?
2 HI!
step 1
step 2
step 3
map, filter, reduce and a famous surprise New tab

`sort()` without a callback compares items as strings, so `"100"` comes before `"20"`. A compare callback fixes it.

Console

(nothing was printed)

Expected console output

[ 240, 90, 600, 160 ]
[ 45, 80 ]
545
2 cheap items
[ '0:a', '1:b' ]
[ 1, NaN, NaN ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 100, 20, 3 ]
[ 3, 20, 100 ]
Sync vs async callbacks, and a wrapper New tab

The forEach callback runs during the script; the timer callback waits until the script ends, even with a 0 ms delay. `once` is a higher-order function that returns a wrapped function.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

1. script starts
2. forEach callback, item 1
2. forEach callback, item 2
4. script ends
5. timer callback runs later

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Pass the result instead of the function

Write setTimeout(showMessage(), 1000);.

terminal
$ Console
── what you'll see ──
The message appears immediately, not after one second.

Break #2

Pass something that isn't a function

Write [1, 2, 3].map("double").

terminal
$ Console
── what you'll see ──
Uncaught TypeError: string "double" is not a function

Myth vs fact

Myth

Callbacks are always asynchronous.

Fact

Many run synchronously, like the ones passed to map, filter and forEach. Only APIs like timers, events and network requests call them later.

Myth

setTimeout(fn, 0) runs fn immediately.

Fact

It runs fn only after the current script (and other queued work) has finished, never in the middle of it.

Myth

Higher-order functions are slower than loops, so avoid them.

Fact

For normal-sized data the difference is negligible and engines optimise them well. Prefer the clearer code; measure before optimising hot paths.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Callback-heavy async code nests deeply ("callback hell") and makes error handling awkward; Promises and async/await (the async phase) were designed to flatten it. Node's older APIs use the error-first callback convention: fn(err, result).

  • ▸

    map, filter and forEach skip holes in sparse arrays and read length once at the start; items appended during iteration aren't visited. Array.prototype.sort is guaranteed stable since ES2019.

  • ▸

    Passing a method as a callback loses its this: setTimeout(obj.method, 0) calls it with this set to the global object (sloppy) or undefined (strict). Use an arrow (() => obj.method()) or obj.method.bind(obj).

  • ▸

    Creating a new arrow inside a render loop or on every call is cheap for the engine, but in UI libraries it changes function identity each time, which can defeat memoisation (React's useCallback exists for this).

Remember this

  1. 1

    Functions in JavaScript are first-class values (also called first-class citizens): anything you can do with a number or a string, you can do with a function. Store it in a variable (const f = greet;), put it in an array or object, pass it as an argument, and return it from another function. Under the hood a function is a special kind of object, which is why it can have properties like name and length.

  2. 2

    A callback is a function you pass to another function so that function can call it ("call it back") at the right moment. You pass the function itself, without parentheses: button.addEventListener("click", handleClick). Writing handleClick() would run it right now and pass its result instead (the bug from Topic 3.1).

  3. 3

    A higher-order function (HOF) is a function that takes a function as an argument, returns a function, or both. You already know some: addEventListener and setTimeout take callbacks; makeCounter and multiplier from Topic 3.6 return functions. The array methods are the everyday HOFs: forEach(fn) runs fn on each item, map(fn) builds a new array of results, filter(fn) keeps items where fn returns something truthy, reduce(fn, start) folds a list into one value, and sort(compareFn) uses your function to compare pairs (the arrays phase covers them all in depth).

  4. 4

    Callbacks can run synchronously or asynchronously. A synchronous callback runs right away, before the HOF returns: map and forEach call your function for every item and only then move on. An asynchronous callback runs later, after the current code has finished: setTimeout(fn, 0) and event listeners hand fn to the browser, which calls it when the timer fires or the click happens. Even a 0 ms timer waits until the current script is done (the event loop, covered in the async phase).

  5. 5

    When a HOF calls your callback, it decides which arguments to pass. map passes three: the item, its index and the whole array. That's harmless for most callbacks, but surprising when the callback accepts optional extra parameters: ["1", "2", "3"].map(parseInt) gives [1, NaN, NaN], because parseInt receives the index as its second parameter, the radix (number base): parseInt("2", 1) is NaN. Wrap it to control the arguments: .map(s => parseInt(s, 10)), or use Number.

  6. 6

    Returning functions lets you build behaviour from parts. A function that wraps another adds a feature around it without changing it: once(fn) runs fn at most once, logged(fn) prints every call, memoize(fn) caches results. This is called a decorator or wrapper. Combined with closures, HOFs let you write code that says *what* to do (prices.filter(isCheap)) instead of spelling out *how* with loops and counters every time. This style is called declarative and is the heart of functional programming.

Explain it without notes

01

What does it mean that functions are first-class in JavaScript?

02

What is a callback, and what is a higher-order function? Give two built-in examples of each idea.

03

What is the difference between a synchronous and an asynchronous callback?

04

Why does ["1", "2", "3"].map(parseInt) return [1, NaN, NaN]?

Practice

01

Write applyTwice(fn, value) that returns fn(fn(value)), and test it with n => n + 3 and 5.

02

Using filter and map, turn ["apple", "kiwi", "banana", "fig"] into the upper-case names of fruits longer than 3 letters.

03

Write logged(fn) that returns a new function which prints "result: " and the result each time, and returns the result.

Trade-offs

  • ↔

    Chains like filter().map().reduce() read clearly but loop over the data several times and create intermediate arrays; a single for loop is faster for very large arrays in hot code.

  • ↔

    Inline arrows keep logic next to where it's used; named functions can be reused, tested on their own and show up by name in stack traces.

Done when you can

  • I can store, pass and return functions.

  • I can pass a callback without accidentally calling it.

  • I can use map, filter and reduce with arrow callbacks.

  • I can say whether a callback runs now or later.

  • I can write a higher-order function that wraps another function.