Skip to main content

Functions

A function is a reusable block of code that performs a specific task. Functions are the fundamental building block of any JavaScript program.

Function declarations​

The classic way to define a function:

function greet(name) {
return `Hello, ${name}!`;
}

const message = greet("Ada");
console.log(message);

Result:

Hello, Ada!

Anatomy:

  • function - the keyword
  • greet - the function name
  • (name) - the parameter list
  • return - sends a value back to the caller
  • greet("Ada") - calling the function with the argument "Ada"

Functions without a return value​

If a function does not return anything, it returns undefined:

function logMessage(text) {
console.log(`[LOG] ${text}`);
}

const result = logMessage("Server started");
console.log(result);

Result:

[LOG] Server started
undefined

Function expressions​

You can also assign a function to a variable:

const add = function (a, b) {
return a + b;
};

console.log(add(3, 4));

Result:

7

The difference from a declaration: this function expression cannot be called before the const line runs (explained below).

Arrow functions​

A shorter syntax introduced in ES6:

const multiply = (a, b) => {
return a * b;
};

console.log(multiply(3, 4));

Result:

12

Implicit return​

If the body is a single expression, you can omit the braces and return:

const double = (n) => n * 2;
console.log(double(5));

const greet = (name) => `Hello, ${name}!`;
console.log(greet("Grace"));

Result:

10
Hello, Grace!

Single parameter shorthand​

With exactly one parameter, you can omit the parentheses:

const square = n => n * n;
console.log(square(4));

Result:

16

When to use which​

SyntaxBest for
function declarationNamed functions, top-level functions, functions that need hoisting
function expressionAssigning to variables, passing as arguments
Arrow =>Short callbacks, inline functions, methods that do not need their own this

Arrow functions have a different this behavior compared to regular functions. This matters when working with objects ( covered in the Objects chapter). For now, arrow functions are your go-to for short, simple functions.

Parameters and arguments​

Parameters are the names listed in the function definition. Arguments are the actual values passed to the function.

Default parameters​

Provide fallback values for missing arguments:

function greet(name = "stranger") {
return `Hello, ${name}!`;
}

console.log(greet("Ada"));
console.log(greet());

Result:

Hello, Ada!
Hello, stranger!

Rest parameters​

Collect any number of arguments into an array:

function sum(...numbers) {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}

console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));

Result:

6
100

The rest parameter must be the last parameter:

function log(level, ...messages) {
for (const msg of messages) {
console.log(`[${level}] ${msg}`);
}
}

log("INFO", "Server started", "Listening on port 3000");

Result:

[INFO] Server started
[INFO] Listening on port 3000

Return values​

A function can return any value - numbers, strings, booleans, arrays, objects, even other functions:

function getUser() {
return {
name: "Ada",
age: 36,
};
}

const user = getUser();
console.log(user.name);
console.log(user.age);

Result:

Ada
36

Early return​

return exits the function immediately. Use it to handle edge cases first:

function divide(a, b) {
if (b === 0) {
return "Cannot divide by zero";
}
return a / b;
}

console.log(divide(10, 2));
console.log(divide(10, 0));

Result:

5
Cannot divide by zero

This pattern - handling errors early and returning - is called a guard clause. It keeps your code flat instead of deeply nested.

Scope​

Scope determines where a variable is accessible.

Block scope (let and const)​

Variables declared with let or const are confined to the nearest {} block:

if (true) {
const message = "inside block";
console.log(message);
}

// console.log(message); // ReferenceError: message is not defined

Result:

inside block

Function scope​

Variables declared inside a function are not accessible outside it:

function example() {
const secret = 42;
console.log(secret);
}

example();
// console.log(secret); // ReferenceError: secret is not defined

Result:

42

Global scope​

Variables declared outside any function or block are global - accessible everywhere:

const appName = "MyApp";

function printAppName() {
console.log(appName); // can access the global variable
}

printAppName();

Result:

MyApp

Minimize global variables. They can be modified from anywhere, making bugs hard to track.

Scope chain​

Inner functions can access variables from outer functions:

function outer() {
const x = 10;

function inner() {
const y = 20;
console.log(x + y); // inner can access x from outer
}

inner();
// console.log(y); // ReferenceError: y is not defined
}

outer();

Result:

30

Hoisting​

Function declarations are hoisted - you can call them before they appear in the code:

console.log(greet("Ada"));

function greet(name) {
return `Hello, ${name}!`;
}

Result:

Hello, Ada!

Function expressions and arrow functions stored in let or const cannot be called before initialization:

// console.log(add(1, 2)); // ReferenceError: Cannot access 'add' before initialization

const add = (a, b) => a + b;
console.log(add(1, 2));

Result:

3

The add binding exists before that line, but it is in the temporal dead zone (TDZ) until initialization finishes. var declarations are hoisted too, but only the declaration - not the assignment - so calling a var function expression early fails differently. This is another reason to avoid var.

Closures​

A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished executing:

function createCounter() {
let count = 0;

return function () {
count++;
return count;
};
}

const counter = createCounter();
console.log(counter());
console.log(counter());
console.log(counter());

Result:

1
2
3

The inner function "closes over" the count variable. Each call to createCounter() creates a new, independent counter:

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 - independent from counterA

Result:

1
2
1

Closures are fundamental to JavaScript. They power patterns like data privacy, factory functions, and event handlers.

Practical closure: private state​

function createWallet(initialBalance) {
let balance = initialBalance;

return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}

const wallet = createWallet(100);
console.log(wallet.getBalance());
console.log(wallet.deposit(50));
console.log(wallet.withdraw(30));
console.log(wallet.getBalance());
// balance is not directly accessible
// console.log(wallet.balance); // undefined

Result:

100
150
120
120

Callbacks​

A callback is a function passed as an argument to another function:

function doTask(taskName, onComplete) {
console.log(`Starting: ${taskName}`);
// ... do work ...
onComplete(taskName);
}

function handleComplete(name) {
console.log(`Finished: ${name}`);
}

doTask("Download file", handleComplete);

Result:

Starting: Download file
Finished: Download file

Callbacks are commonly used with array methods (next chapter) and asynchronous operations.

Anonymous callbacks​

You can pass an arrow function directly instead of defining a named function:

doTask("Process data", (name) => {
console.log(`Done processing: ${name}`);
});

Result:

Starting: Process data
Done processing: Process data

Higher-order functions​

A higher-order function is a function that takes a function as an argument or returns a function. You have already seen both:

  • doTask takes a callback - higher-order (takes a function)
  • createCounter returns a function - higher-order (returns a function)
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}

repeat(3, (i) => console.log(`Step ${i}`));

Result:

Step 0
Step 1
Step 2

Higher-order functions are everywhere in JavaScript - array methods like map, filter, and reduce (covered in the next chapter) are all higher-order functions.

Pure functions​

A pure function always returns the same output for the same input and has no side effects:

// Pure - same input always gives same output
function add(a, b) {
return a + b;
}

// Impure - modifies external state
let total = 0;
function addToTotal(amount) {
total += amount; // side effect
return total;
}

console.log(add(2, 3)); // always 5
console.log(add(2, 3)); // always 5

console.log(addToTotal(10)); // 10
console.log(addToTotal(10)); // 20 - different result for same input

Result:

5
5
10
20

Prefer pure functions when possible - they are easier to test, debug, and reason about.

Immediately Invoked Function Expressions (IIFE)​

A function that runs immediately after it is defined:

(function () {
const secret = "hidden";
console.log(`IIFE running with secret: ${secret}`);
})();

Result:

IIFE running with secret: hidden

IIFEs were historically used to create private scope before let/const and modules existed. You will see them in older code.

Summary​

  • Function declarations are hoisted; expressions and arrow functions are not.
  • Arrow functions => provide concise syntax with implicit return for single expressions.
  • Default parameters provide fallback values; rest parameters collect extra arguments into an array.
  • Guard clauses (early returns) keep code flat and readable.
  • Scope determines variable accessibility: block, function, or global.
  • Closures let inner functions remember outer variables - key for private state and factories.
  • Callbacks are functions passed to other functions - foundational to async JavaScript.
  • Pure functions are predictable and testable - prefer them when possible.

Next up: Arrays - ordered collections of data and the powerful methods to work with them.