JavaScript Basic Functions: Declarations, Calls, Return Values, and Arguments
Functions are the unit of reusable behavior in JavaScript. Almost everything you do in real code — from clicking a button, to fetching an API, to transforming an array — runs inside a function call.
This lesson is the foundation. We cover what a function is, the three syntaxes to create one, how to call them, how arguments and return values work, and the most common patterns you'll see in real code. The deep dives — hoisting, scope, this, closures — come in Module 2.
What a function actually is #
A function is a value. Like a number or a string, you can assign it to a variable, pass it as an argument, return it from another function, and store it in a data structure.
The difference: a function value is callable. Putting () after it (with any arguments) runs it and produces a result.
const greet = function (name) {
return `Hello, ${name}!`;
};
const message = greet('Ada'); // calls the function
console.log(message); // 'Hello, Ada!'
Everything else about functions is built on top of those two facts: they are values, and you call them with ().
Three ways to create a function #
JavaScript has three syntaxes. They produce functions that mostly behave the same — with a few important differences we'll cover here and expand on in Module 2.
1. Function declaration #
function add(a, b) {
return a + b;
}
The oldest and most explicit. Notable properties:
- Hoisted in full — the function is callable anywhere in its scope, even on lines above the declaration.
- Has its own
this(bound at call time). - The function name is visible inside the function (useful for recursion).
2. Function expression #
const add = function (a, b) {
return a + b;
};
A function value assigned to a variable. The function is anonymous (the variable's name doesn't become the function's name — though debuggers in 2026 infer it from the assignment).
- Not hoisted as callable. The variable binding follows
const/letTDZ rules. - Otherwise identical to a declaration.
3. Arrow function #
const add = (a, b) => a + b;
The modern, concise form. Introduced in ES2015. Now the default for most short functions.
Three things that make arrows different:
- No own
this. An arrow inheritsthisfrom the surrounding scope. (Covered in detail in Module 2.6.) - No own
arguments. Use rest parameters (...args) instead — covered in Module 2.2. - Cannot be used as a constructor.
new (() => {})throws.
When the body is a single expression, the braces and return are optional:
const double = n => n * 2; // implicit return
const noop = () => {}; // empty arrow
const sum = (a, b) => a + b; // expression body
const greet = name => `Hi, ${name}`; // single param — parens optional
const longer = n => {
const result = n * 2;
return result; // explicit return needed with braces
};
Most teams default to arrows for everything except top-level utility functions, where function declarations are still common because they show up nicely in stack traces.
Calling a function #
The call site is always funcExpression(arg1, arg2, ...).
add(2, 3); // direct call
obj.method(x); // method call
(() => 1)(); // immediately-invoked function expression (IIFE)
arr.map(double); // function passed as argument
The parentheses are what trigger execution. Without them, you're just referring to the function value.
const fn = () => 'hi';
console.log(fn); // [Function: fn] — the function object
console.log(fn()); // 'hi' — the result of calling it
Forgetting () is the classic "why does this output function instead of the value I want?" bug.
Parameters and arguments #
A function declares parameters in its definition. A caller passes arguments at the call site.
function greet(greeting, name) { // parameters
return `${greeting}, ${name}!`;
}
greet('Hello', 'Ada'); // arguments
JavaScript is permissive about the count:
- Too few arguments — missing parameters are
undefined. - Too many arguments — extras are ignored (but accessible via
argumentsin non-arrow functions, or...restin any function).
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
greet('Hello'); // 'Hello, undefined!'
greet('Hello', 'Ada', 'extra'); // 'Hello, Ada!' — 'extra' ignored
This is convenient (you can call old APIs with extra args without breaking them) and also a source of bugs. TypeScript catches this for you; plain JS doesn't.
Default parameters #
Give any parameter a default value with =:
function greet(name = 'stranger', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
greet(); // 'Hello, stranger!'
greet('Ada'); // 'Hello, Ada!'
Defaults kick in only when the argument is undefined. They are NOT a || fallback — 0, '', and false are passed through.
Rest parameters #
Collect any remaining arguments into an array:
function sumAll(...nums) {
return nums.reduce((s, n) => s + n, 0);
}
sumAll(1, 2, 3, 4); // 10
The ...nums parameter must be last. It gives you a real array (unlike the old arguments object).
We go deeper on parameters in Lesson 2.2.
Return values #
A function returns whatever value follows return. If return is omitted, or used without a value, the function returns undefined.
function add(a, b) {
return a + b;
}
function logIt(x) {
console.log(x);
// no explicit return
}
add(1, 2); // 3
logIt('hi'); // logs 'hi', returns undefined
A function can return any value: a primitive, an object, an array, another function, even itself.
function makeCounter() {
let n = 0;
return function () {
n++;
return n;
};
}
const tick = makeCounter();
console.log(tick()); // 1
console.log(tick()); // 2
console.log(tick()); // 3
That's a function returning a function. Each call to makeCounter() creates a fresh n that the returned function closes over. We get into the mechanics — closures — in Lesson 2.5.
Early returns #
Returning from anywhere stops the function immediately. This is the cleanest way to handle edge cases.
function divide(a, b) {
if (b === 0) return NaN;
return a / b;
}
We saw the early-return pattern in Lesson 1.5; functions are where it really shines.
Functions as values: passing them around #
Because functions are values, you can pass them as arguments. This is the foundation of higher-order functions, which we explore in Lesson 2.7.
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // arrow passed to map
setTimeout(() => console.log('hi'), 1000); // arrow passed to setTimeout
button.addEventListener('click', handleClick); // named handler passed in
In every case, you're passing a function reference — the function itself, not the result of calling it.
A common bug: calling the function instead of passing it.
// BUG — calls handleClick immediately and passes the (likely undefined) return
button.addEventListener('click', handleClick());
// Right — passes the function so it can be called later
button.addEventListener('click', handleClick);
If handleClick needs arguments, wrap it in a thin function or use .bind():
button.addEventListener('click', () => handleClick(specificArg));
button.addEventListener('click', handleClick.bind(null, specificArg));
A function naming style guide #
Language-wise, anything is a valid function name. By convention:
- camelCase for regular functions:
processOrder,getUserById. - PascalCase for functions that create something (classes, factories):
User,createWidget. is/has/canprefix for predicates that return a boolean:isActive,hasPermission,canEdit.- Verbs for actions, nouns for getters.
saveUser()does something.currentUser()returns something.
Good names make code readable; bad ones force readers to open the function to understand the call site.
When to extract a function #
Three practical signals you should pull code into a function:
- You're writing the same thing twice. Even before the third time. Two is enough for the cost-benefit to favor extraction.
- You want to name what some code does. If you find yourself writing
// calculate taxas a comment, that's the function name. Replace the comment with a function call. - A block has grown beyond a screen. Long functions are hard to test, hard to understand, hard to change.
The inverse — when not to extract — is when the function will be called once, the body is two lines, and naming it would obscure intent. "Just inline it" is sometimes the right answer.
What's next #
That completes Module 1. You now know:
- What JavaScript is and where it runs (1.1)
- Variables —
let,const,var(1.2) - Data types (1.3)
- Operators (1.4)
- Conditionals (1.5)
- Loops (1.6)
- Functions, the basics (this lesson)
Module 2 dives much deeper on functions — hoisting, lexical scope, closures, this, higher-order patterns, and pure functions. With Module 1's foundation in place, those concepts will land much more cleanly than they would in isolation.
Try it yourself #
The "call it vs. pass it" distinction is the most common function bug. Predict the output:
const sayHi = () => console.log('Hi');
setTimeout(sayHi, 100);
setTimeout(sayHi(), 200);js_sandboxThe output is Hi immediately (from the second line), then Hi 100 ms later (from the first line).The first
setTimeout passes the function reference — setTimeout calls it after 100 ms.The second one passes the result of calling
sayHi(). The function runs now, logs 'Hi', and the second setTimeout receives undefined — which it silently treats as a no-op after 200 ms.Two characters of difference (() vs. nothing). Two completely different behaviors. Internalizing this distinction makes the rest of higher-order JavaScript click into place.
Up next in JavaScript
More from this topic
Enjoyed this article?
Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.


