Let's Start With The Basics What Is A Function, Really?
Before callbacks make any sense, you need to feel functions in your bones. Not the "yeah I've typed function a hundred times" kind of familiarity, but the kind where you actually understand what's happening when you write one.
So here's the core idea, stripped all the way down:
A function is a block of instructions that sits there, doing nothing, until you tell it to run.
That's it. Writing a function doesn't run anything. It just stores a set of steps under a name so you can trigger them later, as many times as you want, without retyping them every single time.
function greet() {
console.log('Hey there, welcome!');
}At this point, nothing has happened. JavaScript has simply noted down "okay, if anyone calls greet, run this code." Nothing prints. Nothing executes. It's just sitting there.
greet(); // Now it actually runsThis is the part that flips the switch. The parentheses are what say "run it now." Without them, you're just pointing at the function. With them, you're firing it.
This distinction between defining and calling is genuinely one of the most important things to internalize early, because callbacks live and die by this exact difference.
Giving Functions Something To Work With Parameters
A function that does the exact same thing every single time isn't all that useful. What if you want to greet different people, with different names, every time?
That's where parameters come in. Parameters are placeholders inside your function that get filled in with real values when you call it. They turn a fixed function into a flexible one.
function greet(name) {
console.log('Hey there, ' + name + '!');
}
greet('Amara'); // Hey there, Amara!
greet('Ruguru'); // Hey there, Ruguru!
greet('Manny'); // Hey there, Manny!Same function definition, three completely different outputs, because each call passed in a different value for name. The function itself never changed. Only the input changed.
Getting Something Back With The return Keyword
Functions can also hand something back to whoever called them. This is where return comes in.
return is the function saying "I'm done, and here's the result of my work, take it." Without return, a function might still do useful work internally, console logs, calculations, whatever, but it won't give you anything you can store, pass around, or use later.
function addTax(price) {
return price * 1.16; // 16% VAT
}
let totalPrice = addTax(1000);
console.log(totalPrice); // 1160Here, addTax calculates a value and immediately hands it back through return. We catch that returned value in totalPrice, and now we can do whatever we want with it: log it, pass it somewhere else, use it in more math.
This single concept, returning a value so it can be used elsewhere, is the foundation for everything that's about to come next.
The Plot Twist Functions Are Just Values
Here's the part that rewires how you see JavaScript entirely, so don't skim this bit.
In JavaScript, functions are values. Not "special syntax." Not "a different category of thing." Actual values, the same category as numbers, strings, objects, and arrays.
What does that actually mean in practice? It means you can:
- Store a function inside a variable
- Pass a function into another function as an argument
- Return a function out of another function
All of that is completely normal, everyday JavaScript.
// Storing a function in a variable
const sayHi = function () {
console.log('Hi!');
};
sayHi(); // Hi!This function doesn't have its own name. It's what's called an anonymous function, a function expression with no identifier of its own. But because we assigned it to sayHi, we now have a name we can call it by. Functionally, it behaves exactly the same as a named function.
You'll also see this written using arrow function syntax, which is shorter but means exactly the same thing:
const sayHi = () => {
console.log('Hi!');
};Same function. Same behavior. Just a more compact way of writing it. Once functions click as "values you can pass around," callbacks stop being a weird special concept and start being an obvious consequence of how JavaScript already works.
Now The Real Thing What Is A Callback?
A callback is a function that you hand over to another function, so that function can run it at exactly the right moment, on its own schedule.
Here's the key shift in thinking: normally when you call a function, you're passing it data, numbers, strings, objects. With a callback, you're passing it behavior. You're saying "here's a set of instructions, run these whenever it makes sense for you."
function doSomethingAndThenCall(callback) {
console.log('Doing the thing...');
callback(); // Now we call the function that was passed in
}
function announce() {
console.log('The thing is done!');
}
doSomethingAndThenCall(announce);Output:
Doing the thing...
The thing is done!Notice something important: we passed announce, not announce(). No parentheses. This is critical. Without the parentheses, we're handing over the function itself, the set of instructions. With parentheses, announce() would run immediately, and we'd be passing whatever it returns instead, which in this case is undefined, since announce doesn't return anything.
This one detail, parentheses or no parentheses, trips up almost everyone at first. Get comfortable with it now and a huge chunk of confusing callback bugs later just disappear.
Let's Build A Real Thing Our Own Version Of map
You've almost certainly used .map() before, the built in array method that transforms every item in an array and gives you back a new array. Most people use it without ever seeing what's happening underneath. So let's build our own version from scratch. This is genuinely where everything we've covered so far locks into place.
The Goal
Take an array of numbers and double every single one of them.
const numbers = [1, 2, 3, 4, 5];
// We want: [2, 4, 6, 8, 10]Building customMap
function customMap(array, callback) {
const result = []; // Start with an empty array to collect results
for (let i = 0; i < array.length; i++) {
const transformed = callback(array[i]); // Run the callback on each item
result.push(transformed); // Add the transformed value to our results
}
return result; // Hand back the new array
}Let's pull this apart piece by piece:
arrayis the list of items we want to transformcallbackis the function that defines how each item should be transformed- We loop through every index, run that item through
callback, and push whatever comes back intoresult - At the end, we return the fully built array
Notice that customMap itself has zero idea what transformation it's performing. It doesn't know about doubling, tripling, or anything else. It only knows "take each item, run it through whatever function I was given, collect the output." That separation is the entire point.
Now let's actually use it:
function double(number) {
return number * 2;
}
const doubled = customMap(numbers, double);
console.log(doubled); // [2, 4, 6, 8, 10]We pass double in as the callback, without calling it. customMap loops through numbers, calls double on each one, and builds up the result array.
Swap The Callback, Get A Different Result
Want triple instead of double? You don't touch customMap at all. You just hand it a different callback.
function triple(number) {
return number * 3;
}
const tripled = customMap(numbers, triple);
console.log(tripled); // [3, 6, 9, 12, 15]Same loop, same structure, same customMap, completely different output, purely because the callback changed. This is the whole superpower of callbacks in one example.
Writing It Inline With Arrow Functions
Most of the time, when your transformation is short, you won't bother writing a separate named function at all. You'll just write the callback directly where it's needed:
const doubled = customMap(numbers, (number) => number * 2);
console.log(doubled); // [2, 4, 6, 8, 10]This does exactly the same thing as passing double, we're just defining the function and passing it in a single step, right at the call site. Once a callback gets short enough, this becomes the natural way to write it.
The Real Deal How Array.prototype.map Actually Works
Now that you've built customMap from scratch, the built in Array.prototype.map method should feel a lot less mysterious. It's doing the same fundamental job: loop through an array, run a callback on each item, collect the results into a new array, and hand that new array back.
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled); // [2, 4, 6, 8, 10]But since this is a real method living on every array, it's worth going a level deeper than "it loops and transforms," because there are details that genuinely matter once you start using it for real work.
The Callback Actually Receives Three Arguments
When map calls your callback, it doesn't just hand you the current item. It can hand you up to three things:
array.map((element, index, array) => {
// element: the current item being processed
// index: the position of that item in the array
// array: the original array map was called on
});Most of the time you'll only need element. But index becomes useful surprisingly often, for example, if you want to pair each item with its position:
const fruits = ['apple', 'banana', 'cherry'];
const numbered = fruits.map((fruit, index) => `${index + 1}. ${fruit}`);
console.log(numbered);
// ['1. apple', '2. banana', '3. cherry']The third argument, the original array, is rarely needed, but it's there if your callback needs to reference the full array while processing a single element.
map Always Returns A New Array, Same Length
This is one of the most important guarantees map makes: the array it returns will always be the same length as the array you started with. Every input item produces exactly one output item, even if that output is undefined.
const values = [1, 2, 3];
const result = values.map((value) => {
if (value > 1) return value * 10;
// no return for value === 1
});
console.log(result); // [undefined, 20, 30]Notice the array still has three slots. map doesn't skip or remove anything based on what your callback returns. If you need to drop items entirely, map is the wrong tool, that's a job for filter.
map Does Not Change The Original Array
map is non destructive. It builds a brand new array and leaves the original completely untouched.
const original = [1, 2, 3];
const doubled = original.map((n) => n * 2);
console.log(original); // [1, 2, 3] — unchanged
console.log(doubled); // [2, 4, 6] — new arrayThis matters a lot in practice, especially when working with frameworks that rely on detecting changes to data. map gives you a fresh array every time, which is exactly the kind of predictable behavior that makes code easier to reason about.
Chaining map With Other Array Methods
Because map returns a real array, you can immediately chain other array methods onto the result. This is incredibly common in real codebases.
const numbers = [1, 2, 3, 4, 5, 6];
const result = numbers
.map((number) => number * 2) // [2, 4, 6, 8, 10, 12]
.filter((number) => number > 5) // [6, 8, 10, 12]
.reduce((total, number) => total + number, 0); // 36
console.log(result); // 36Each method in the chain takes the output of the one before it. map transforms, filter narrows down, reduce collapses everything into a single value. Once map feels natural, reading chains like this becomes second nature.
map Versus forEach — Don't Mix These Up
A common mistake is using map when you don't actually need the returned array. If you're just looping through an array to do something, like logging values or making API calls, and you don't care about collecting results, forEach is the more honest tool.
// Misusing map — the returned array is thrown away
numbers.map((number) => console.log(number));
// Better — forEach communicates "I'm just looping, not transforming"
numbers.forEach((number) => console.log(number));Both technically "work," but map signals to anyone reading your code "this produces a new array I care about." If you're not using that new array, reach for forEach instead. Code that says what it means is easier for everyone, including future you, to understand.
Bringing It All Together
Here's the full journey, end to end:
- Functions are reusable blocks of instructions that only run when called
- Parameters let the same function behave differently depending on what's passed in
returnlets a function hand a result back so it can be used elsewhere- Functions are values, which means they can be stored, passed around, and handed to other functions
- A callback is a function passed into another function so it can be run at the right time
Array.prototype.mapis callbacks in action: it loops over an array, runs your callback on each item with(element, index, array), and returns a brand new array of the same length, leaving the original untouched
Once callbacks click, a huge amount of JavaScript stops feeling like memorized syntax and starts feeling like a pattern you recognize everywhere, in array methods, in event listeners, in promises, in pretty much every library you'll ever touch. You're no longer just writing code that runs top to bottom. You're handing off instructions and trusting other code to run them exactly when it should.
That shift in thinking is the real unlock here.
