What a Function Is, and Why You Want One
A function is a piece of code you write once and run whenever you need it. Defining a function does nothing on its own — the code inside simply sits there. Later you call it, by writing its name followed by brackets, and only then does the body run. That gap between defining and calling is the whole point: you decide the steps in one place and choose the moment they happen somewhere else entirely.
The obvious benefit is that you stop repeating yourself. If GST is applied in six places on a checkout page and the rate changes, six edits mean five chances to miss one. A single calculateGst() function means one edit. The less obvious but bigger benefit is that a function gives a name to an idea. const payable = applyDiscount(subtotal, coupon) tells a reader what is happening without them reading a single line of the arithmetic inside.
A function also creates a boundary. Names declared inside it are invisible outside, so you can use i or temp in one function without worrying about what any other function calls its variables. That containment is what lets a large program stay readable — you can study one function at a time and trust that it cannot secretly reach into the rest.
The shape never changes: the function keyword, a name, a list of parameters in brackets, and a body in braces. Calling it means writing the name and brackets. Forgetting the brackets is a real bug, not a typo the language catches for you — calculateGst is the function itself, and calculateGst(1000) is the number it produces.
// Define once
function calculateGst(amount) {
return amount * 0.18;
}
// Call as many times as you like
console.log(calculateGst(1000)); // 180
console.log(calculateGst(2500)); // 450
// The brackets are what actually runs it
console.log(typeof calculateGst); // 'function' - the function itself
console.log(calculateGst(1000)); // 180 - the result of running it Parameters, Arguments and Defaults
The names inside the brackets of the definition are parameters — placeholders for values that do not exist yet. The values you supply when calling are arguments. People use the two words loosely in conversation, but the distinction is simply definition-side versus call-side, and it does come up in interviews.
JavaScript never checks how many arguments you passed. Pass too few and the missing parameters are undefined; pass too many and the extras are dropped without a word. This is a genuine source of quiet bugs. calculateArea(5) on a function expecting width and height gives NaN, because 5 * undefined is NaN, and that NaN then spreads through every calculation downstream. Nothing throws, so the failure surfaces far away from its cause.
Default parameters fix the common half of that problem. Writing function greet(name = 'Guest') means that when name arrives as undefined, it becomes 'Guest' instead. Notice the exact trigger: the default fires for undefined only. Passing null deliberately keeps null, and passing 0 or an empty string keeps those too. That is the same distinction as ?? versus || from the previous lesson, and it is the behaviour you want — a deliberate zero should survive.
Arguments are passed by value. For a number, string or boolean that means the function receives a copy and cannot touch your original. For an object or array, the value being copied is the reference, so the function ends up pointing at the very same object you hold — and if it changes a property, you see the change. Functions that quietly modify the objects handed to them are among the hardest bugs to trace, so either make the mutation obvious in the function's name or copy the object before changing it.
function calculateArea(width, height) {
return width * height;
}
console.log(calculateArea(5, 3)); // 15
console.log(calculateArea(5)); // NaN - height is undefined
console.log(calculateArea(5, 3, 99)); // 15 - the extra argument is ignored
// Defaults fire only for undefined
function greet(name = 'Guest') {
return `Welcome, ${name}`;
}
console.log(greet()); // 'Welcome, Guest'
console.log(greet(undefined)); // 'Welcome, Guest'
console.log(greet(null)); // 'Welcome, null' - null is a real value
console.log(greet('')); // 'Welcome, '
// Objects are shared with the function, not copied
function addGstInPlace(order) {
order.total = order.total * 1.18; // edits the caller's object
}
const order = { total: 1000 };
addGstInPlace(order);
console.log(order.total); // 1180 - A default value may refer to a parameter declared before it —
function page(limit = 10, skip = limit)is legal. It may not refer to one declared after it, because that hits the same temporal dead zone thatletdoes.
return Is the Only Way a Function Answers You
return does two things at once: it hands a value back to whoever called the function, and it stops the function immediately. Any code after a return on the same path never runs. Both halves get used constantly, and the stopping half is what makes the early-return pattern possible.
A function with no return still returns something — undefined. That matters more than it sounds, because it is very easy to write a function that prints a result instead of returning it, then wonder why the caller has nothing to work with. console.log(x) shows a value to you, the developer, on a screen you will not have in production. return x gives the value to the program. Almost every beginner mixes these up at least once, and the symptom is always identical: the console output looks perfectly correct while the variable holds undefined.
The early-return pattern uses the stopping half deliberately. Instead of wrapping the real work inside a deep if, deal with the invalid and edge cases first and return out of them. What remains reads as a short list of guards followed by one clear main path, rather than a pyramid of nested braces that you have to unwind mentally.
One trap here is worth memorising. JavaScript inserts a semicolon after a bare return at the end of a line. So if you write return and put the value you want on the next line, the function returns undefined and the value below is quietly discarded — no error, no warning. Always keep the value on the same line as return, or at least open the bracket on that line.
function getGrade(marks) {
// Guards first, each returning out of the function
if (typeof marks !== 'number' || Number.isNaN(marks)) return 'Invalid input';
if (marks < 0 || marks > 100) return 'Out of range';
// The main path, with nothing nested around it
if (marks >= 75) return 'Distinction';
if (marks >= 60) return 'First class';
if (marks >= 40) return 'Pass';
return 'Fail';
}
console.log(getGrade(82)); // 'Distinction'
console.log(getGrade(-5)); // 'Out of range'
// Printing is not returning
function showGrade(marks) {
console.log(getGrade(marks)); // displays it, hands back nothing
}
const result = showGrade(82);
console.log(result); // undefined
// Automatic semicolon insertion swallows this
function broken() {
return
{ grade: 'Pass' }; // never reached
}
console.log(broken()); // undefined - If a function exists to produce a value, it should
returnthat value and print nothing. Keepingconsole.logout of your logic functions is what makes them reusable — the same function then works in a page, in a Node script and in a test without modification.
Declaration, Expression, Arrow — and When the Difference Bites
There are three ways to create a function and they are not interchangeable. A declaration begins with the function keyword at the start of a statement. A function expression stores a function in a variable. An arrow function is a shorter expression syntax added in ES6, written with =>.
The first practical difference is hoisting, from Lesson 2. A declaration is hoisted whole, body included, so you can call it on a line above where it is written. A function stored in a const follows the const rule and is unusable until that line has run, so calling it earlier throws ReferenceError: Cannot access 'triple' before initialization. Neither behaviour is wrong, but it means moving a function up or down a file can break code that was silently relying on hoisting.
Arrow functions have shorthand worth learning properly rather than half-remembering. With exactly one parameter the brackets around it are optional. When the body is a single expression, the braces and the word return can both be dropped and the expression's value is returned automatically — that is called an implicit return. Returning an object literal implicitly needs an extra pair of brackets, because otherwise the braces are read as a function body: () => ({ ok: true }).
The second difference is the one that actually causes bugs, and Lesson 20 covers it in full: an arrow function does not get its own this. It borrows the this of the code surrounding it. That makes arrows ideal for callbacks written inside a method, and wrong for defining a method on an object, where you want this to mean the object itself. Use a regular function for methods; use an arrow for the small callbacks inside them.
// Declaration - hoisted completely, callable from above
console.log(double(4)); // 8, even though the definition is below
function double(n) {
return n * 2;
}
// Function expression - not usable until this line has run
const triple = function (n) {
return n * 3;
};
// Arrow function, full form
const quadruple = (n) => {
return n * 4;
};
// Arrow shorthand: one parameter, one expression, implicit return
const half = n => n / 2;
console.log(half(9)); // 4.5
// Returning an object literal needs brackets around it
const makeUser = name => ({ name: name, active: true });
console.log(makeUser('Ananya')); // { name: 'Ananya', active: true } function f() {}— declaration; hoisted; has its ownthisconst f = function () {}— expression; not hoisted; has its ownthisconst f = () => {}— arrow; not hoisted; borrowsthisfrom around it- One parameter lets you drop the brackets:
n => n * 2 - A single-expression body lets you drop the braces and
return - To return an object implicitly, wrap it:
() => ({ a: 1 }) - Methods on objects: regular function. Callbacks inside them: arrow.
- Arrow functions cannot be called with
new, and they have noargumentsobject of their own. An error readingis not a constructorusually means an arrow was used where a regular function was expected.
Scope, and the Closures That Come With It
Every function creates its own scope. Variables declared inside are invisible from outside, which is exactly what stops two functions interfering with each other. The reverse is not true: a function can freely read the variables of the scope that contains it. This one-way visibility is called lexical scoping, and "lexical" simply means it is decided by where the code is written, not by who calls it or when.
A closure is what you get when an inner function keeps using an outer function's variables after the outer function has already finished. Most people expect those variables to vanish the moment the outer function returns. They do not — as long as an inner function still refers to them, they stay alive in memory. This sounds theoretical and is not. It is how you build a counter nobody can tamper with, how an event handler remembers which list item it belongs to, and how nearly every JavaScript library keeps private state without classes.
Read the counter below carefully. makeCounter declares count and returns a function that increments it. By the time you call next(), makeCounter has long since finished — yet count is still there, still remembering. Call makeCounter() a second time and you get a completely separate count, because each call creates a fresh scope. There is no way to read or change count from the outside except through the returned function, which is the simplest form of data hiding the language offers.
The classic demonstration of getting closures wrong uses var in a loop with setTimeout. Because var is function-scoped, all three timers close over the same single i. By the time the timers actually run the loop has finished and i is 3, so you see 3, 3, 3. Change var to let and you get 0, 1, 2, because let creates a fresh binding on every iteration for the callbacks to capture. This exact snippet is one of the most frequently asked JavaScript interview questions, and the answer they want is the explanation, not the output.
function makeCounter() {
let count = 0; // private - nothing outside can reach it
return function () {
count = count + 1; // still alive after makeCounter finished
return count;
};
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2
const other = makeCounter();
console.log(other()); // 1 - a separate closure with its own count
// The classic var-in-a-loop trap
for (var i = 0; i < 3; i++) {
setTimeout(function () { console.log('var:', i); }, 0);
}
// prints: var: 3 var: 3 var: 3
for (let j = 0; j < 3; j++) {
setTimeout(function () { console.log('let:', j); }, 0);
}
// prints: let: 0 let: 1 let: 2 - Both loops above print after every other line in the file, even with a delay of 0. Why that happens is the event loop, and Lesson 21 explains it properly.
Functions as Values: Callbacks and Rest Parameters
Functions in JavaScript are values, exactly like numbers and strings. You can store one in a variable, put one in an array, hand one to another function, and return one from a function — which is precisely what makeCounter did. This is what people mean when they say functions are "first-class" in JavaScript, and it is the foundation of everything in Lesson 10 on array methods.
A function passed to another function so that it can be called later is a callback. addEventListener('click', handleClick) and [1, 2, 3].map(double) are both this pattern. Notice that you pass handleClick, not handleClick(). Adding brackets calls the function immediately and passes whatever it returned — so the handler runs once during setup and then never again, and no error is reported. That single pair of brackets is one of the most common beginner bugs in event code.
When a function should accept any number of arguments, gather them with a rest parameter: function sum(...numbers) collects every argument into a genuine array, so all the array methods work on it. A rest parameter must be the last one in the list. It replaces the older arguments object, which looked like an array without being one and does not exist inside arrow functions at all.
The demo below puts the whole lesson together in the shape you should actually write code: one function that takes marks and returns a grade, one arrow function with a default parameter that calculates a percentage, and a third that does nothing but read the input and display the result. Keeping the logic separate from the display is the habit that makes functions reusable.
// A function stored in a variable and passed as a value
const double = n => n * 2;
console.log([1, 2, 3].map(double)); // [2, 4, 6]
// Rest parameter: any number of arguments, gathered into a real array
function sum(...numbers) {
return numbers.reduce((running, n) => running + n, 0);
}
console.log(sum(10, 20, 30)); // 60
console.log(sum()); // 0
// Pass the function, do not call it
function runTwice(action) {
action();
action();
}
function sayHi() {
console.log('Hi');
}
runTwice(sayHi); // correct - prints Hi twice
// runTwice(sayHi()); // wrong - sayHi runs once now, then TypeError HTML
<div class="fn-demo">
<h3>Marks to grade</h3>
<input type="number" id="marks" value="82" placeholder="Marks out of 100">
<button onclick="showGrade()">Get grade</button>
<div id="out"></div>
</div> CSS
.fn-demo { padding: 20px; background: #f0f0f0; border-radius: 8px; font-family: system-ui, sans-serif; }
input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: 180px; }
button { padding: 10px 20px; background: #d1039e; color: white; border: none; border-radius: 5px; cursor: pointer; margin-left: 6px; }
#out { margin-top: 15px; padding: 15px; background: white; border-radius: 5px; border-left: 4px solid #d1039e; min-height: 24px; } JavaScript
// Pure logic: takes a number, returns a string. Prints nothing.
function getGrade(marks) {
if (Number.isNaN(marks)) return 'Enter a number';
if (marks < 0 || marks > 100) return 'Out of range';
if (marks >= 75) return 'Distinction';
if (marks >= 60) return 'First class';
if (marks >= 40) return 'Pass';
return 'Fail';
}
// Arrow function with a default parameter and an implicit return
const percentOf = (marks, outOf = 100) => (marks / outOf) * 100;
// This one does the reading and the showing - nothing else
function showGrade() {
const raw = document.getElementById('marks').value.trim();
const out = document.getElementById('out');
if (raw === '') {
out.textContent = 'Enter a number';
return; // early return, nothing below runs
}
const marks = Number(raw);
out.innerHTML = '<strong>' + getGrade(marks) + '</strong>' +
'<br>Percentage: ' + percentOf(marks).toFixed(1) + '%';
} - Try changing
percentOf(marks)topercentOf(marks, 50)in the demo. The default parameter steps aside the moment you supply a second argument, which is how one function serves both a paper marked out of 100 and one marked out of 50.
