So far your code has run top to bottom in one straight line. Real programs are messier: you click a button and something happens later, a timer fires after a delay, or you ask the browser to run a function once data arrives. In each case you hand a function to someone else and say "call this back when you're ready."
A callback is a function passed as an argument to another function so it can be invoked later. Because functions are first-class values in JavaScript — they can be stored in variables, passed around, and returned from other functions — callbacks are the glue that holds asynchronous code together.
Closures are the other half of the story. A closure lets a function remember the variables from its surrounding scope even after that scope has finished executing. Together, callbacks and closures are the foundation of privacy, factories, and most async patterns in JavaScript.
flowchart TD OUT["makeCounter() count = 0"] --> RET["returns inner fn"] RET --> CLO["closure holds count"] CLO --> CALL["next() count += 1"] style OUT fill:#e0900b,color:#fff style CLO fill:#e0900b,color:#fff
Functions as values
Before you can pass a function, you have to believe it is just another value. These two snippets do the same job, but the second stores the function in a variable first:
function greet(name) {
return "Hello, " + name;
}
const greet = function (name) {
return "Hello, " + name;
};
Once it lives in a variable, you can hand it to another function. Array methods like map and filter already do this; setTimeout does it for timing, and event listeners do it for interaction. The pattern is everywhere.
When you pass a function, you are passing a reference to the function object, not a copy of its body. That means the receiving function can store it, call it multiple times, or ignore it entirely. Understanding that functions are ordinary objects — they just happen to be callable — makes callbacks feel less magical.
function sayLater(message) {
setTimeout(() => {
console.log(message);
}, 10);
}
sayLater("Hello from the future");
function runTwice(fn) {
fn();
fn();
}
runTwice(() => console.log("tick"));
Closures: remembering where you came from
So how does that remembering actually work? When JavaScript creates a function, it bundles up the scope it was born in and carries that bundle along for the function's whole life. That bundle is the closure. The variables are not copied — the inner function holds a live reference to the very same bindings, which is why two closures over the same scope see each other's changes.
This is how factory functions work. You call an outer function, it sets up some local state, and it returns an inner function that still has access to that state:
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
Even though makeCounter finished running and its local scope technically ended, the returned function keeps a reference to count. Each call increments the same hidden variable.
The mechanism is straightforward: every function carries a hidden pointer to the lexical environment where it was created. When you call the function, JavaScript looks up variables in that environment if they are not found in the local scope. Because the environment persists as long as any function references it, the variables stay alive.
flowchart TD
subgraph A["makeCounter() call A"]
CA["count = 0"] --> FA["nextA()"]
end
subgraph B["makeCounter() call B"]
CB["count = 0"] --> FB["nextB()"]
end
FA -->|"nextA()==> 1, 2, 3..."| RA["independent"]
FB -->|"nextB()==> 1, 2..."| RB["independent"]
style A fill:#1e293b,color:#fff
style B fill:#1e293b,color:#fff
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a());
console.log(a());
console.log(b());
console.log(a());
Data privacy with closures
Because the inner function is the only thing that can touch the outer variable, closures give you data privacy without classes or special keywords. The variable cannot be read or written from outside — only the returned function can change it.
This pattern appears everywhere: event handlers that remember which button they belong to, timers that track how many times they fired, and module patterns that hide implementation details. It is one of the most powerful ideas in JavaScript, and it emerges naturally from the rules of lexical scope.
A practical example is a validator builder. Instead of writing one giant validation function, you write a factory that returns small, focused validators, each closing over its own threshold or message:
function makeMinValidator(min) {
return function (value) {
return value >= min;
};
}
const isAdultAge = makeMinValidator(18);
Here min is locked inside the closure. The returned function is small, testable, and carries its configuration with it.
function makeMinValidator(min) {
return function (value) {
return value >= min;
};
}
const isAdultAge = makeMinValidator(18);
const isPassingScore = makeMinValidator(60);
console.log(isAdultAge(21));
console.log(isAdultAge(16));
console.log(isPassingScore(75));
console.log(isPassingScore(45));
What does this print? Each call to the returned function increments the same hidden counter.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const c = makeCounter();
console.log(c());
console.log(c());
The first call increments count from 0 to 1 and returns it.
The second call increments the same count from 1 to 2.
Write makeSecretHolder(secret) that returns an object with two methods: get() returns the secret, and set(newSecret) updates it. The secret must not be accessible from outside the object.
function makeSecretHolder(secret) {
// return an object with get() and set(newSecret)
}
Use a closure so the secret variable stays private.
Return an object with two methods that both close over the secret parameter.
function makeSecretHolder(secret) {
return {
get: function () {
return secret;
},
set: function (newSecret) {
secret = newSecret;
}
};
}
Write makeMultiplier(factor) that returns a new function. The returned function takes one number and multiplies it by factor. So makeMultiplier(3)(4) should return 12.
function makeMultiplier(factor) {
// return a function
}
Return a function from makeMultiplier.
The inner function closes over
factorand uses it in the multiplication.
function makeMultiplier(factor) {
return function (n) {
return n * factor;
};
}
This loop is supposed to print 0, 1, 2 with a small delay, but it prints 3 three times because of a closure bug. Fix it by changing exactly one keyword.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10);
}
The bug is caused by
varbeing function-scoped, so all callbacks share onei.Change
vartoletso each iteration gets its own binding.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10);
}
Write makeAdder(initial) that returns a function. The returned function takes a number, adds it to a running total that starts at initial, and returns the new total. So const add = makeAdder(10); add(5); add(3); should return 18 on the second call.
function makeAdder(initial) {
// return a function that adds to a running total
}
Use a closure variable
totalinitialised toinitial.The inner function updates
totaland returns it.
function makeAdder(initial) {
let total = initial;
return function (n) {
total += n;
return total;
};
}
Recap
- A callback is a function passed to another function to be called later.
- A closure lets an inner function remember variables from its outer scope even after the outer function finishes.
- Closures enable data privacy — hide state inside a factory and expose only the methods you want.
varin a loop creates one shared variable;letcreates a fresh binding per iteration.- Factory functions that return configured closures are a powerful, testable pattern for building validators, counters, and stateful utilities.
Next you'll learn how to branch your code with conditionals and repeat work with loops.