Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
What will I learn
- You will learn what "first-class functions" actually means, and why it is the key idea that gives JavaScript its character;
- how to store functions in variables, arrays, and objects, just like any other value;
- how to pass a function as an argument to another function (the pattern behind
map,filter, event handlers, and timers); - how to return a function from a function, and build small, tailored functions on the fly;
- the difference between referring to a function and calling it -- a one-character distinction that causes real, hard-to-spot bugs.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-14 read, so functions, objects, and bracket access are second nature.
Difficulty
- Beginner
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions (this post)
Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
Welcome to Phase 2. In episode 14 we closed Phase 1 by building a real command-line tool, and I promised that the next stretch would go deep on the single feature that gives JavaScript its real character. Here it is. Everything in this phase -- closures, higher-order functions, callbacks, currying, the whole async model we reach later -- grows from one deceptively small idea: in JavaScript, functions are values.
That sentence sounds almost too simple to matter. It does not. Once it truly clicks, a huge chunk of the language that used to look like magic (why you can hand a function to setTimeout, why map takes a function, why event handlers work the way they do) suddenly reads as the most natural thing in the world. So we are going to take it seriously and slowly, because this is the foundation the rest of Phase 2 is built on top of.
What "first-class" means
When we say a language has first-class functions, we mean functions are treated exactly like any other value -- a number, a string, an object. You can store a function in a variable, put it in an array, make it a property of an object, pass it into another function as an argument, and return it out of a function as a result. There is nothing special or ceremonial about it. A function is just data that happens to be callable.
We have quietly been relying on this since episode 1, every single time we wrote const greet = function () { ... }. That line is not "defining a function" in some separate namespace -- it is creating a function value and binding it to a const, the same way const x = 5 binds a number. Let's now do it deliberately and watch how much power falls out of it:
const sayHi = function () {
return "hi";
};
console.log(typeof sayHi); // "function" - it is a value, and it has a type
console.log(sayHi); // [Function: sayHi] - the function itself
console.log(sayHi()); // "hi" - the () runs it and gives back its return value
Look carefully at those last two lines, because they are the whole episode in miniature. The name sayHi holds a function value. Reading sayHi gives you the function itself -- an object you can pass around. Adding () invokes it and gives you back whatever it returns. Keep that pair in your head: sayHi is the thing, sayHi() is the result of running the thing. We will come back to it hard in a few minutes, because getting them confused is one of the most common beginner bugs there is.
Functions in variables, arrays, and objects
Because a function is an ordinary value, it can live anywhere a value can live. In a variable, obviously -- we just did that. But also inside an array, sitting right next to numbers and strings as if it were one:
const operations = [
(n) => n + 1,
(n) => n * 2,
(n) => n - 3,
];
console.log(operations.length); // 3 - it is a normal array of three items
console.log(operations[1](10)); // 20 - grab the 2nd function, then call it with 10
That double-bracket-then-paren, operations[1](10), trips people up the first time, so let's read it left to right. operations[1] looks up index 1 and hands you back the second function (the doubler). Then the trailing (10) calls that function with 10. Two separate steps glued together: fetch the value, then invoke it. Nothing more mysterious than scores[1] + 5 would be.
Functions also live perfectly happily as object properties -- which, as it happens, is exactly what a "method" is. A method is nothing more special than a function stored under a key:
const calculator = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
};
console.log(calculator.add(2, 3)); // 5 - dot access, then call
const op = "sub";
console.log(calculator[op](9, 4)); // 5 - pick the function by a string key, then call
That last line is quietly powerful, and it leans directly on the bracket-access we learned back in episode 12. We chose which function to run using a string variable, op, at runtime. Think about what that enables: the operation to perform can come from user input, from a config file, from a network message -- anywhere -- and we look it up in an object and run it. A table of functions keyed by name is one of the cleanest tools in the language, and we will build a proper one in a moment.
Passing a function as an argument
Here is where first-class functions stop being a curiosity and start being genuinely useful. Because a function is a value, you can hand one to another function, exactly the way you would hand it a number. The array methods you already met -- map, filter, reduce, forEach -- all work precisely this way: you give them a function, and they call it for you, at the right moments, with the right arguments.
Let's build our own function that accepts a function, so the pattern is not just something the standard library does behind a curtain:
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i); // call the function we were handed, passing the current index
}
}
repeat(3, (i) => console.log(`iteration ${i}`));
// iteration 0
// iteration 1
// iteration 2
Notice that repeat has no idea what action does, and it does not care. Its one job is to call action some number of times and pass it the loop counter. We handed in a small function that logs, but we could hand in anything. That separation -- generic machinery here, specific behaviour handed in there -- is the beating heart of reusable code. A function you accept as a parameter and call later has a name: it is a callback, and it is the pattern we build on for the rest of the series.
The function you pass does not have to be written inline. You can pass a named function by its bare name too, which reads beautifully when the behaviour is reused in more than one place:
function announceStep(i) {
console.log(`step ${i} done`);
}
repeat(2, announceStep); // pass the function BY NAME - no parentheses
// step 0 done
// step 1 done
Read that call again and let it sink in: repeat(2, announceStep), not repeat(2, announceStep()). We are handing repeat the function itself so that repeat can call it whenever it likes. That distinction -- bare name versus name-with-parentheses -- is so important, and so easy to get wrong, that it deserves its own section. Having said that, let's tackle it head on.
The call-versus-reference bug (the one that bites everyone)
There is a critical difference between referring to a function (action) and calling it (action()). Referring to it gives you the function value. Calling it runs the function right now and gives you back whatever it returned. When you want to hand a function to something that will call it later -- a timer, an event handler, an array method -- you pass the bare name, without parentheses:
function boom() {
return "BOOM";
}
const good = boom; // REFERENCE: 'good' now IS the function
const bad = boom(); // CALL: 'bad' is "BOOM", the string it returned
console.log(typeof good); // "function" - we kept the function
console.log(typeof bad); // "string" - we kept the result of running it
console.log(good()); // "BOOM" - 'good' is callable, so we can run it
Now watch how this exact mix-up produces a real bug. Timers and event handlers expect you to give them a function they will call at the right moment. If you accidentically add (), you call the function immediately and hand over its return value instead of the function -- and then the timer has nothing useful to call:
function sayLater() {
console.log("one second has passed");
}
// WRONG: this RUNS sayLater right now, and passes its return value (undefined)
// setTimeout(sayLater(), 1000); // logs immediately, timer gets undefined
// RIGHT: pass the function itself; setTimeout calls it after ~1000ms
// setTimeout(sayLater, 1000); // nothing now, message after one second
(I have left those two lines commented so this block stays a clean, runnable file -- uncomment them in your own console to feel the difference.) The symptom of this bug is almost always the same confused question: "why did my handler run immediately instead of when I clicked / when the timer fired?" The answer is nearly always a stray pair of parentheses. sayLater is the function; sayLater() is what you get by running it. When something else is supposed to do the calling, you give it the bare name and get out of its way.
Returning a function from a function
Now the flip side, and it is where things get properly fun. A function can produce a function. It can build one, customize it, and hand it back to you. A function whose job is to manufacture other functions is often called a factory:
function makeMultiplier(factor) {
return function (n) {
return n * factor;
};
}
const triple = makeMultiplier(3);
const tenX = makeMultiplier(10);
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
console.log(triple(2)); // 6
makeMultiplier(3) returns a brand new function that multiplies its input by 3. makeMultiplier(10) returns a different new function that multiplies by 10. Each returned function remembers the factor it was born with, even though makeMultiplier has long since finished running. That remembering is called a closure, and it is so central that the very next episode is devoted entirely to it. For now, just savour the shape: you can write a function that stamps out specialized functions, tailored to whatever you pass in.
This is not an academic toy. Returning functions lets you write small, general-purpose wrappers that add behaviour to any function you feed them. Here is a classic one, once, which takes a function and returns a new version that will only ever run a single time -- handy for one-off setup that must never happen twice:
function once(fn) {
let called = false;
let saved;
return function (...args) {
if (!called) {
called = true;
saved = fn(...args); // run the real function the first time, remember the result
}
return saved; // every later call just returns that saved result
};
}
const setup = once(() => {
console.log("running expensive setup...");
return 42;
});
console.log(setup()); // "running expensive setup..." then 42
console.log(setup()); // 42 (the setup line does NOT print again)
console.log(setup()); // 42
Look at everything that came together there: once accepts a function (fn), returns a function (the wrapper), and the returned wrapper remembers state (called and saved) between calls via closure. Passing, returning, and remembering -- three ideas from this phase in eleven lines. This is the sort of small, sharp utility that first-class functions make trivial, and that clunkier languages need whole design patterns to imitate.
A dispatch table: retiring the big switch
Let me show you the single most practical everyday payoff of "functions in objects", because you will reach for it constantly. Back in episode 7 we used switch to branch on a value. That is fine for a handful of cases, but a long switch gets ugly fast and is annoying to extend. When each branch is really just "run this bit of code", you can store those bits as functions in an object and look them up by key. This is called a dispatch table:
const handlers = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
div: (a, b) => (b === 0 ? NaN : a / b),
};
function calculate(op, a, b) {
const handler = handlers[op];
if (typeof handler !== "function") {
throw new Error(`unknown operation: "${op}"`);
}
return handler(a, b);
}
console.log(calculate("mul", 6, 7)); // 42
console.log(calculate("div", 10, 0)); // NaN
Compare this to the equivalent switch. Adding a new operation here means adding one line to the handlers object -- no touching the calculate logic at all. The typeof handler !== "function" check is our guard against an unknown key (if op is "pow", handlers["pow"] is undefined, and calling undefined() would throw an ugly TypeError, so we catch it first and throw a clear message instead). Dispatch tables scale gracefully to dozens of cases, they are easy to test one entry at a time, and they read like a tidy list of capabilities rather than a sprawling branch. Quite some real-world routers, command parsers, and state machines are, under the hood, exactly this: an object full of functions and a lookup.
First-class functions in other languages
Many of you came over from the Learn Python Series (and a few from Rust and Go), so a look sideways helps place JavaScript in context. The good news: first-class functions are not a JavaScript invention, and seeing them elsewhere makes the concept feel less like a language quirk and more like a fundamental tool.
Python has first-class functions too, and the factory pattern looks almost identical to ours:
def make_multiplier(factor):
def multiply(n):
return n * factor
return multiply # return the inner function (no parentheses!)
triple = make_multiplier(3)
print(triple(5)) # 15
# functions in a dict - Python's dispatch table
handlers = {
"add": lambda a, b: a + b,
"mul": lambda a, b: a * b,
}
print(handlers["mul"](6, 7)) # 42
Notice the same bare-name-versus-call distinction: return multiply returns the function, return multiply() would call it and return its result. Python even has the identical trap -- sorted(nums, key=len) passes the len function by name, and beginners routinely write key=len() and get an error. The idea travels perfectly.
Rust has first-class functions as well, but its type system makes you name the shapes precisely. A function that returns a function has to spell out that it is returning "some type that implements the callable trait", and closures that capture a variable use the move keyword:
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |n| n * factor // a closure that captures 'factor'
}
fn main() {
let triple = make_multiplier(3);
println!("{}", triple(5)); // 15
}
Where JavaScript lets you sling functions around with zero ceremony, Rust asks you to state the types up front -- more typing, but the compiler then guarantees you never call something that is not callable. Same capability, different amount of paperwork, which is the recurring theme every time we compare these languages.
Go also treats functions as values, and its factory reads very close to JavaScript's, just with explicit type signatures on the parameters and return:
package main
import "fmt"
func makeMultiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}
func main() {
triple := makeMultiplier(3)
fmt.Println(triple(5)) // 15
}
The throughline across all four languages: a function is a value you can store, pass, and return. JavaScript simply has the loosest, most casual syntax for it (no type annotations, no move, no ceremony), which is exactly why the idea can feel invisible in JS until someone points at it -- as I am doing now. ;-)
Why this matters so much
Step back and take in what first-class functions actually buy you. You can:
- Store behaviour in data structures -- a lookup table of functions, an array of steps, a config object full of handlers.
- Pass behaviour into generic algorithms -- give
mapyour transformation, giverepeatyour action, give an event system your reaction. - Generate specialized behaviour at runtime -- a factory that stamps out tailored functions, or a wrapper like
oncethat adds behaviour to any function you feed it.
Languages that lack first-class functions have to reach for heavier machinery -- interfaces, function pointers, anonymous classes, whole design patterns -- to accomplish these same three things. JavaScript makes them natural and nearly invisible. And virtually every elegant pattern you will meet later, from event handling to functional data pipelines to the async control flow that runs the modern web, rests squarely on this one property. Here is a tiny taste that uses all three ideas at once:
const strategies = {
loud: (s) => s.toUpperCase(),
quiet: (s) => s.toLowerCase(),
};
function transform(text, strategy) {
return strategy(text); // behaviour passed in, called here
}
console.log(transform("Hello", strategies.loud)); // "HELLO"
console.log(transform("Hello", strategies.quiet)); // "hello"
We stored functions in an object, picked one out by name, and passed it into a generic transform that neither knows nor cares which strategy it received. That miniature example -- store, select, pass -- is the seed of an enormous amount of real, professional JavaScript. Get genuinely comfortable here, and the forementioned "magic" of the rest of the language stops being magic and starts being obvious.
Try it yourself
Three exercises, increasing in difficulty. Type them out and run them -- reading is not the same as knowing. Full solutions open the next episode.
- Build an object
mathOpsholding functionsadd,sub, andmul. Then writecompute(a, b, opName)that looks up the operation by its string name (bracket access!), guards against an unknown name, and calls it. Testcompute(6, 4, "mul")andcompute(6, 4, "nope"). - Write a function
applyTwice(fn, value)that callsfnonvalue, then callsfnagain on that result, and returns the final answer. Test it with a "double" function and the value3(you should get12), and with a "shout" function that uppercases a string. - Write a factory
makeGreeter(greeting)that returns a function taking anameand returning"<greeting>, <name>!". Create ahigreeter and abyegreeter from it and call each. Then, in one sentence of a comment, explain why writingmakeGreeter("Hi")()on one line (with the extra parentheses) is usually a mistake when what you actually want is a reusable greeter to keep around.
So what did we actually cover?
- First-class functions means functions are ordinary values: storable, passable, and returnable, just like numbers, strings, and objects.
- You can put functions in variables, arrays, and objects; a function stored under an object key is exactly what a "method" is.
- Passing a function to another function (as
map,filter, and ourrepeatdo) lets you hand behaviour into generic code -- the callback pattern, which powers the rest of the series. - Referring to a function (
fn) is not the same as calling it (fn()); pass the bare name when something else should do the calling later, or you will trigger the "why did my handler run immediately?" bug. - A function can return a function (a factory, or a wrapper like
once), producing specialized behaviour at runtime -- powered by closures, which we tackle next. - A dispatch table (an object full of functions, looked up by key) is a clean, scalable replacement for a sprawling
switch. - Python, Rust, and Go all have first-class functions too; JavaScript just has the most casual syntax for them.
Next episode we zoom in on arrow functions versus the function keyword: their syntax differences, and the one big behavioural difference around this that quietly decides which of the two you should reach for.