A function is a named block of code that takes data through parameters, does something and returns a result with return; without return it returns undefined. In JavaScript you can declare a function with function, write it as an expression or as an arrow function (a, b) => a + b. Variables declared inside a function aren't visible outside it, and a nested function remembers the outer function's variables even after it has finished; that's called a closure.
After this you will be able to
- Write a function three ways (declaration, expression and arrow) and explain how they differ in hoisting
- Use default and rest parameters and return a value with
return - Explain scope and closures using a counter as an example
- Pass a function as a callback to
mapandfilterand split a script into pure functions
Know beforehand
Verified with
Node.js 24 LTS (24.18.0)Microsoft Edge 155
Checked against docs: October 11, 2026
Contents
Why functions
In the previous lessons, the tip, discount and delivery calculations were repeated in almost every example. Every repetition is a place to make a mistake and a place you'll have to fix when a rule changes. A function solves both problems: you describe an action once, give it a name and then call it by name with different data. MDN describes a function as a set of statements that performs a task or calculates a value: it takes input and returns an output related to that input.
function calcTip(bill, percent) {
return Math.round(bill * percent / 100);
}
console.log(calcTip(12500, 10)); // → 1250
console.log(calcTip(8990, 15)); // → 1349
console.log(calcTip(4000, 5)); // → 200calcTipis the function's name. A good name is a verb that says what the function does:calcTip,formatPrice,isPaid.billandpercentin the parentheses are parameters: names for the values the function receives when called.calcTip(12500, 10)is a call. The numbers in the parentheses are arguments:12500goes intobill,10intopercent.returnsends the result back to where the function was called from, here intoconsole.log.
Declarations, expressions and arrow functions
JavaScript has three main ways to create a function. The result is similar; they differ in syntax and a couple of important details.
// 1. Function declaration
function toTiyn(tenge) {
return Math.round(tenge * 100);
}
// 2. Function expression
const toTenge = function (tiyn) {
return tiyn / 100;
};
// 3. Arrow function: the short form, no return…
const formatPrice = (tenge) => `${tenge} ₸`;
// …and with a body in braces, where return is required
const priceWithDiscount = (price, percent) => {
const discount = Math.round(price * percent / 100);
return price - discount;
};
console.log(toTiyn(1999.9)); // → 199990
console.log(toTenge(199990)); // → 1999.9
console.log(formatPrice(4990)); // → 4990 ₸
console.log(priceWithDiscount(4990, 10)); // → 4491Declaration
- What it looks like
- function toTiyn(tenge) { … }
- Specifics
- Hoisted: you can call it above the line where it's declared
Expression
- What it looks like
- const toTenge = function (tiyn) { … };
- Specifics
- The function is a variable's value; callable only after that line
Arrow
- What it looks like
- const formatPrice = (tenge) => …;
- Specifics
- Short syntax; without braces the result is returned automatically; no own this
Hoisting only applies to declarations. Per MDN, the interpreter hoists the entire function declaration to the top of its scope, so calling it above the declaration works. Function expressions and arrow functions are values of const variables and can't be used before the line that declares them:
console.log(double(4)); // → 8
function double(n) {
return n * 2;
}
console.log(half(4)); // ReferenceError: the arrow function doesn't exist yet
const half = (n) => n / 2;$ node hoisting.js
8
…\first-js\hoisting.js:7
console.log(half(4)); // ReferenceError: the arrow function doesn't exist yet
^
ReferenceError: Cannot access 'half' before initialization
at …
Node.js v24.18.0Arrow functions have limitations: per MDN, they don't have their own this and aren't used as object methods or constructors. That doesn't matter in this lesson: for short calculations and callbacks, arrow functions are a natural, convenient choice.
Parameters: default values and rest
If you don't pass an argument, the parameter gets undefined. Extra arguments are simply ignored. To avoid checking for undefined by hand, give the parameter a default value right in the declaration: name = "guest". Per MDN, the default is used only when no argument is passed or it's undefined; null counts as a passed value.
function greet(name = "guest", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet("Aliya")); // → Hello, Aliya!
console.log(greet()); // → Hello, guest!
console.log(greet(undefined, "Hi")); // → Hi, guest!
console.log(greet(null)); // → Hello, null!
// A rest parameter collects all passed values into an array
function sum(...prices) {
let total = 0;
for (const price of prices) {
total += price;
}
return total;
}
console.log(sum(1600, 1200, 500)); // → 3300
console.log(sum()); // → 0The rest parameter ...prices collects all passed arguments into an array. You need it when you don't know in advance how many values will arrive: a receipt may have one price or twenty. A rest parameter must come last in the list.
return, and functions without one
return does two things: it hands back a value and ends the function immediately. Code after an executed return doesn't run. That's handy for early exits: deal with the special case first, then write the main path.
function deliveryCost(total) {
if (total >= 15000) {
return 0; // the function ends here
}
return 1500;
}
function logOrder(id) {
console.log(`Order ${id}`); // returns nothing
}
console.log(deliveryCost(23500)); // → 0
console.log(deliveryCost(9000)); // → 1500
const result = logOrder(1042); // → Order 1042
console.log(result); // → undefinedIf a function has no return, or has one without a value, it returns undefined. This is a common beginner bug: the function “calculated something” and printed it to the console, but the variable got undefined. Printing and returning are different actions: console.log shows a value to a person, return passes it on to the program.
Scope and closures
Scope is the part of a program where a variable is available. Per MDN, a function creates its own scope: variables declared inside aren't visible outside. A block in braces, such as the body of an if or a loop, also creates a scope for let and const. Inner code sees everything outside, but not the other way round.
const city = "Almaty"; // global scope
function describeOrder(id) {
const orderStatus = "paid"; // visible only inside the function
if (id > 1000) {
const note = "new customer"; // visible only inside the if block
console.log(`${id}: ${orderStatus}, ${note}, ${city}`);
}
}
describeOrder(1042); // → 1042: paid, new customer, Almaty
console.log(typeof orderStatus); // → undefined
// A parameter with the same name "shadows" the outer variable
let total = 100;
function addTip(total) {
total = total + 10;
return total;
}
console.log(addTip(50)); // → 60
console.log(total); // → 100$ node scope-error.js
…\first-js\scope-error.js:6
console.log(orderId);
^
ReferenceError: orderId is not defined
at …
Node.js v24.18.0Now the key consequence. A function created inside another function keeps access to its variables, even after the outer function has finished. MDN defines a closure as the combination of a function and its lexical environment: the variables that were in scope where it was created.
function createCounter() {
let count = 0; // lives as long as the returned function does
return () => {
count++;
return count;
};
}
const nextTicket = createCounter();
console.log(nextTicket()); // → 1
console.log(nextTicket()); // → 2
const otherQueue = createCounter();
console.log(otherQueue()); // → 1
console.log(nextTicket()); // → 3
// A function that creates functions with a "remembered" parameter
function makeDiscount(percent) {
return (price) => Math.round(price * (100 - percent) / 100);
}
const studentPrice = makeDiscount(15);
const staffPrice = makeDiscount(30);
console.log(studentPrice(2000)); // → 1700
console.log(staffPrice(2000)); // → 1400Each call to createCounter creates a new count variable, and the returned function holds on to that one. That's why nextTicket and otherQueue count independently. You can't change count from outside: only the returned function can reach it. That's how closures help hide data and create “preconfigured” functions like studentPrice and staffPrice.
Functions as values: callbacks, map and filter
In JavaScript, a function is a value just like a number or a string: you can store it in a variable, pass it to another function and return it from a function. A function passed to another function to be called by it is a callback. The most common examples are the array methods filter and map.
const menu = [
{ name: "Cappuccino", price: 1600 },
{ name: "Latte", price: 1800 },
{ name: "Americano", price: 1200 },
{ name: "Raf", price: 2200 },
];
// filter keeps the elements for which the function returns true
const affordable = menu.filter((item) => item.price < 1700);
// map turns each element into a new one
const names = affordable.map((item) => item.name);
console.log(names.join(", ")); // → Cappuccino, Americano
// Returning an object from an arrow function — wrap it in parentheses
const raised = menu.map((item) => ({ ...item, price: item.price + 200 }));
console.log(raised[0].price); // → 1800
console.log(menu[0].price); // → 1600
// You can pass your own function as a value too
const formatPrice = (tenge) => `${tenge} ₸`;
console.log(menu.map((item) => item.price).map(formatPrice).join(" | ")); // → 1600 ₸ | 1800 ₸ | 1200 ₸ | 2200 ₸filtercalls the callback for each element and keeps those for which it returned a truthy value.mapcalls the callback for each element and builds a new array from what the callback returned.- Both methods create a new array, and per MDN
filterreturns a shallow copy: the objects inside are the same ones. - To return an object from a short arrow function, wrap it in parentheses:
(item) => ({ ...item }). Without them, the braces are read as the function body.
Pure functions and side effects
A side effect is anything a function does besides returning a value: changing an outer variable, printing to the console, writing a file, calling a server. A function with no side effects whose result depends only on its arguments is called pure. It's easier to test: the same arguments always give the same result.
// An impure function: reads and changes an outer variable
let cartTotal = 0;
function addToCart(price) {
cartTotal += price;
return cartTotal;
}
console.log(addToCart(1600)); // → 1600
console.log(addToCart(1600)); // → 3200
// A pure function: the result depends only on the arguments
const addPrice = (total, price) => total + price;
console.log(addPrice(0, 1600)); // → 1600
console.log(addPrice(0, 1600)); // → 1600Side effects are necessary; without them a program shows and saves nothing. The practice we follow in InterLAB projects: keep calculations in pure functions and gather output, requests and writes in a few places. Then a discount or delivery fee can be checked with a single call, without running the whole program.
With functions you now have all the core building blocks of the language: values, conditions, loops and a way to assemble them into named actions. Next in the track are data structures: arrays, objects, Map and Set and their methods. Once your functions start working with the network and files, you'll need the lesson on error handling, and closures come in handy in the debounce and throttle recipe.
How this was verified2 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.