How to buildOctober 11, 20266 min read
JavaScript from scratchStep 4 of 9

Functions: parameters, return values, scope

In short

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.

TutorialLevel: BeginnerFunctionsPractice: ~45–90 min

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 map and filter and 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
  1. 01Why functions
  2. 02Declarations, expressions and arrow functions
  3. 03Parameters: default values and rest
  4. 04return, and functions without one
  5. 05Scope and closures
  6. 06Functions as values: callbacks, map and filter
  7. 07Pure functions and side effects

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.

tip-function.js
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));     // → 200
The calculation is described once and called three times with different data.
  • calcTip is the function's name. A good name is a verb that says what the function does: calcTip, formatPrice, isPaid.
  • bill and percent in 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: 12500 goes into bill, 10 into percent.
  • return sends the result back to where the function was called from, here into console.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.

three-forms.js
// 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));    // → 4491
All three forms create regular functions that are called the same way.

Declaration

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:

hoisting.js
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;
The first line works; the third doesn't.
terminal
$ 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.0
The path is shortened and stack lines are omitted. 8 was printed before the error: it happened at runtime.

Arrow 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.

params.js
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());                      // → 0
greet(null) doesn't fall back to “guest”: null is a value, not the absence of one.

The 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.

return.js
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);               // → undefined
logOrder prints text but returns nothing, so calling it yields undefined.

If 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.

scope.js
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
The function sees the global city, but orderStatus doesn't exist outside the function.
terminal
$ node scope-error.js
…\first-js\scope-error.js:6
console.log(orderId);
            ^

ReferenceError: orderId is not defined
    at …

Node.js v24.18.0
orderId is declared inside createOrder and isn't available outside. The path is shortened and stack lines are omitted.

Now 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.

closure.js
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));   // → 1400
Each counter has its own count variable; makeDiscount “remembers” the percentage.

Each 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.

callbacks.js
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 ₸
filter and map don't change the original menu array; they create new ones.
  • filter calls the callback for each element and keeps those for which it returned a truthy value.
  • map calls the callback for each element and builds a new array from what the callback returned.
  • Both methods create a new array, and per MDN filter returns 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.

pure.js
// 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));   // → 1600
addToCart was called twice with the same argument and returned different values; addPrice always returns the same.

Side 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.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.