Functions
Sooner or later you notice the same few lines showing up in three, four, five spots across a script. Maybe you flash a friendly greeting when a visitor signs in, again when they sign out, and once more somewhere in the middle. Copy-pasting that block everywhere means every future tweak has to be made everywhere too. Miss one spot and the behavior drifts.
Functions are the fix. They are the main building blocks of a program: you write a piece of logic once, give it a name, and run it as many times as you want from wherever you want.
You’ve already been calling functions someone else wrote — alert(message), prompt(message, default), confirm(question) are all built into the browser. Now you’ll write your own.
Function Declaration
The most common way to make a function is a function declaration. It reads left to right in a fixed shape:
function showMessage() {
alert( 'Hello everyone!' );
}
Four parts, always in this order: the function keyword, the name you pick, a list of parameters in parentheses (comma-separated, empty here), and the body — the code to run — inside curly braces.
The general template looks like this:
function name(parameter1, parameter2, ... parameterN) {
// body
}
Defining a function doesn’t run it. The body just sits there until you call it by writing its name followed by parentheses: showMessage().
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();
Each call executes the body from the top. Two calls, two alerts. That’s the whole point of a function on display: write the greeting once, fire it as often as you need.
The payoff shows up when requirements change. Want a different message, or a fancier dialog instead of alert? You edit the one function body, and every call across the codebase picks up the change automatically.
Local variables
A variable declared with let (or const) inside a function lives only inside that function. Nobody on the outside can see it.
function showMessage() {
let message = "Hello, I'm JavaScript!"; // local variable
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- Error! The variable is local to the function
The last line throws a ReferenceError. As far as the outside world is concerned, message never existed — it was born when the call started and vanished when the call finished.
Outer variables
A function can reach out and read variables declared in the surrounding code:
let userName = 'Maya';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, Maya
Inside showMessage, userName isn’t local, so JavaScript looks one level up and finds it in the outer scope. Reading works — and so does writing:
let userName = 'Maya';
function showMessage() {
userName = "Theo"; // (1) changed the outer variable
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // Maya before the function call
showMessage();
alert( userName ); // Theo, the value was modified by the function
Because there’s no local userName, the assignment on line (1) reaches back out and overwrites the outer one. After the call, the change is visible everywhere.
That only happens while there’s no local variable of the same name. The moment you declare a local userName, it shadows the outer one — the function works with its own copy and leaves the outer variable untouched:
let userName = 'Maya';
function showMessage() {
let userName = "Theo"; // declare a local variable
let message = 'Hello, ' + userName; // Theo
alert(message);
}
// the function will create and use its own userName
showMessage();
alert( userName ); // Maya, unchanged, the function did not access the outer variable
The rule in one line: a function uses a local variable if one exists; otherwise it falls back to the nearest outer one.
Parameters
Parameters let you feed data into a function so it can behave differently on each call. List them in the parentheses, separated by commas.
function showMessage(from, text) { // parameters: from, text
alert(from + ': ' + text);
}
showMessage('Priya', 'Hello!'); // Priya: Hello! (*)
showMessage('Priya', "What's up?"); // Priya: What's up? (**)
When the call runs at (*) and (**), the values you pass are copied into the local variables from and text. Inside the body they behave exactly like local variables that happened to arrive pre-filled.
Type a sender and a message below, then call the function. The same showMessage runs each time — only the arguments change.
That word — copied — matters. A function receives a copy of each value, so reassigning a parameter inside the function can’t reach back and change the caller’s variable:
function showMessage(from, text) {
from = '*' + from + '*'; // make "from" look nicer
alert( from + ': ' + text );
}
let from = "Priya";
showMessage(from, "Hello"); // *Priya*: Hello
// the value of "from" is the same, the function modified a local copy
alert( from ); // Priya
The outer from stays "Priya". The function decorated its own private copy and threw it away when it returned.
Default values
Call a function with fewer arguments than it declares, and the missing ones become undefined. That’s not an error:
showMessage("Priya");
With the earlier definition, that prints "*Priya*: undefined" — text was never passed, so it’s undefined, and string concatenation turns it into the text "undefined".
Often you’d rather supply a sensible fallback. Add = after a parameter to give it a default value used when the argument is omitted:
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}
showMessage("Priya"); // Priya: no text given
The default also kicks in when the argument is explicitly undefined, since that’s indistinguishable from “not passed”:
showMessage("Priya", undefined); // Priya: no text given
A default doesn’t have to be a plain literal. It can be any expression, even a function call, and it’s only evaluated when the argument is actually missing:
function showMessage(from, text = anotherFunction()) {
// anotherFunction() only executed if no text given
// its result becomes the value of text
}
Alternative default parameters
Sometimes you’d rather decide the fallback in the body instead of the parameter list — maybe the logic is involved, or depends on other parameters computed first. You can compare against undefined yourself:
function showMessage(text) {
// ...
if (text === undefined) { // if the parameter is missing
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message
Or reach for ||:
function showMessage(text) {
// if text is undefined or otherwise falsy, set it to 'empty'
text = text || 'empty';
...
}
When the only values you want to reject are null and undefined — but 0 or an empty string should pass through as real input — the nullish coalescing operator ?? is the precise tool:
function showCount(count) {
// if count is undefined or null, show "unknown"
alert(count ?? "unknown");
}
showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown
Notice showCount(0) prints 0, not "unknown". With || it would have printed "unknown", because 0 is falsy. That difference is exactly why ?? exists.
Try each argument below and watch the two fallbacks disagree exactly when the value is 0 or "" — falsy, but not missing.
Returning a value
A function can hand a result back to the code that called it, using return. The classic example adds two numbers:
function sum(a, b) {
return a + b;
}
let result = sum(1, 2);
alert( result ); // 3
return a + b computes the sum and ships it out. The call sum(1, 2) evaluates to 3, which lands in result.
Change either number and the returned value updates live. The function has no idea where its result goes — it just hands one back.
return can appear anywhere in the body. The instant execution hits it, the function stops and control jumps back to the caller with the given value. A function can have several return statements, and only the first one reached actually fires:
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('Do you have permission from your parents?');
}
}
let age = prompt('How old are you?', 18);
if ( checkAge(age) ) {
alert( 'Access granted' );
} else {
alert( 'Access denied' );
}
You can also write return with no value to bail out of a function early:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
alert( "Showing you the movie" ); // (*)
// ...
}
If checkAge(age) is falsy, the bare return exits before line (*), so the movie never shows. This “guard clause” pattern keeps the happy path un-indented and easy to follow.
Naming a function
A function does something, so its name usually reads as a verb. Keep it short, accurate, and descriptive enough that a reader understands the action without opening the body.
A widely-followed habit is to start the name with a verb prefix that hints at the kind of work. Teams agree on what each prefix means and stick to it. Functions beginning with "show", for instance, tend to show something.
Common prefixes:
"get…"— return a value,"calc…"— calculate something,"create…"— create something,"check…"— check something and return a boolean.
Some names built from them:
showMessage(..) // shows a message
getAge(..) // returns the age (gets it somehow)
calcSum(..) // calculates a sum and returns the result
createForm(..) // creates a form (and usually returns it)
checkPermission(..) // checks a permission, returns true/false
With a shared vocabulary of prefixes, a glance at a call site tells you both what the function does and roughly what it gives back.
Functions == Comments
Short functions that each do one thing are a joy to read. When one thing grows too big, that’s a hint to split it into smaller named functions. It isn’t always obvious how, but it pays off.
A well-named helper is easier to test and debug — and its name doubles as a comment. Compare two versions of showPrimes(n), both printing the prime numbers below n.
The first inlines the primality test with a nested loop and a label:
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert( i ); // a prime
}
}
The second pulls the test into its own function, isPrime:
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // a prime
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
The second reads better. Where the first has a knot of nested loops and a label, the second just says isPrime(i) — the name is the explanation. Code like this is often called self-describing.
Here is that self-describing version running. Drag the slider to change n; showPrimes leans entirely on the well-named isPrime helper.
So it’s worth creating a function even when you’ll call it exactly once. Structure and readability are reasons enough.
Summary
A function declaration has this shape:
function name(parameters, delimited, by, comma) {
/* code */
}
- Values passed in as arguments are copied into the function’s local variables.
- A function can read outer variables, but the visibility is one-way: outside code cannot see anything declared inside.
- A function can return a value. With no
return, or an empty one, the result isundefined.
For clean code, lean on parameters and local variables rather than reaching out to modify outer ones. A function that takes inputs, works with them, and returns a result is far easier to follow than one that silently mutates global state as a side effect.
On naming:
- The name should describe what the function does, so a call site reads clearly.
- Functions are actions, so names are usually verbs.
- Lean on well-known prefixes —
create…,show…,get…,check…— to signal intent.
Functions are the main building blocks of scripts. You’ve got the basics now, which is enough to start writing real ones. We’ll keep coming back to them and dig into their more advanced powers as the course goes on.