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 keywordgreet-- the function name(name)-- the parameter listreturn-- sends a value back to the callergreet("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: function expressions are not hoisted (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
| Syntax | Best for |
|---|---|
function declaration | Named functions, top-level functions, functions that need hoisting |
function expression | Assigning 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 are NOT hoisted:
// console.log(add(1, 2)); // ReferenceError: Cannot access 'add' before initialization
const add = (a, b) => a + b;
console.log(add(1, 2));
Result:
3
var declarations are hoisted too, but only the declaration -- not the assignment. 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:
doTasktakes a callback -- higher-order (takes a function)createCounterreturns 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.