JavaScript Functions: Declarations, Arrow Functions, Parameters and return
We learn function declarations, expressions and arrow functions, parameters and arguments, default parameters, return, passing functions as callbacks, function scope and how to write good functions.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
Since the start of the series we've been using ready-made functions like console.log, Math.round and sort. In this post we write our own. First, look at this small piece of code:
function greet(name) {
console.log(`Hello ${name}`);
}
const result = greet();
console.log(result);
How many lines show up in the console, and what do they say? Do you get an error, or does the code quietly run and print something? Make your guess before you keep reading. This little snippet actually asks two separate questions; we'll answer both in the sections on parameters and return.
What Is a Function and How Do You Define One?
Think of a function as a coffee machine. You put some things in (beans, water), the machine does the job it knows, and it gives you a result (coffee). You don't need to know what's inside the machine every time; pressing the button is enough. A function is likewise a named piece of code you write once and run as many times as you like.
The most classic way to define one is the function declaration. Paste this into your browser's console and try it:
function sayHello() {
console.log("Hello!");
}
sayHello(); // Hello!
sayHello(); // Hello!
The function keyword, a name, parentheses, and a body inside curly braces. Defining a function doesn't run it; it just puts the machine on the counter. To run it, or call it, you put parentheses after its name. sayHello without parentheses is the machine itself; sayHello() is pressing the button.
Function declarations have a small privilege: you can call them even before the line where they're defined. Before running the code, the engine makes a pass over these definitions and gets them ready. This behavior is called hoisting; we'll see how it works in detail in the Intermediate series.
Parameters and Arguments
For a function to take values from outside, you write parameters inside the parentheses. A parameter is a placeholder used inside the function. The actual value you pass when calling it is called an argument:
function greet(name, city) {
console.log(`Hello ${name}, how is ${city}?`);
}
greet("Ada", "Izmir"); // Hello Ada, how is Izmir?
greet("Grace"); // Hello Grace, how is undefined?
greet("Alan", "Bursa", "extra"); // Hello Alan, how is Bursa?
Here name and city are parameters, and "Ada" and "Izmir" are arguments. JavaScript isn't picky at all about the number of arguments. Pass too few and there's no error; any parameter without a matching argument becomes undefined. Pass too many and the extras are ignored.
That solves the first half of the opening question. We called greet() without any argument, so name became undefined and the first line printed Hello undefined.
The clean way to prevent this is default parameters, added in ES2015. You write a default value next to the parameter with an equals sign; if no argument is passed, that value is used:
function greet(name = "guest") {
console.log(`Hello ${name}`);
}
greet("Ada"); // Hello Ada
greet(); // Hello guest
return: Getting a Result Back
The functions so far did something but didn't give us anything back. If you want to run a calculation and get its result, you use return:
function calculateTotal(price, quantity) {
return price * quantity;
}
const total = calculateTotal(150, 3);
console.log(total); // 450
console.log(calculateTotal(20, 2) + 5); // 45
return does two things: it sends the value after it back to where the function was called, and it ends the function right there. Lines after return don't run. In the coffee machine example, it's the moment the machine hands you the cup.
The second half of the opening question is answered here too. greet has a console.log inside but no return. A function without return always returns undefined. So result was assigned undefined, and that's what the second line printed. Printing to the console and returning a value are not the same thing. console.log shows you the result; return hands the result over to the rest of your code. By the way, if you see one more faded undefined at the bottom of the console, don't be surprised; that's the return value of the console.log on the last line, since it doesn't return anything either.
The early return idea we pointed to in the post on conditions pays off here. If you rule out the invalid case at the very start and leave the function, the rest of the code doesn't drown in nested ifs:
function getDiscount(total) {
if (total < 100) {
return 0;
}
if (total < 500) {
return total * 0.05;
}
return total * 0.1;
}
console.log(getDiscount(80)); // 0
console.log(getDiscount(200)); // 10
console.log(getDiscount(1000)); // 100
Function Expressions and Arrow Functions
You can also define a function by assigning it to a variable. This is called a function expression:
const square = function (number) {
return number * number;
};
console.log(square(4)); // 16
The difference from a declaration is that it lives inside a variable. Since it's defined with const, the TDZ rule we saw in the post on variables applies: call it before the line where it's defined and you get a ReferenceError.
Arrow functions, added in ES2015, are a shorter way to write the same idea. The function keyword goes away, and an arrow (=>) goes between the parameters and the body:
const square = (number) => {
return number * number;
};
const double = number => number * 2;
const add = (a, b) => a + b;
const getRandom = () => Math.random();
console.log(square(4)); // 16
console.log(double(5)); // 10
console.log(add(2, 3)); // 5
The shortening rules are simple. With a single parameter you can drop the parentheses; with no parameters or more than one, the parentheses are required. If the body is a single expression you can drop the curly braces and the return; the result of that expression is returned automatically. The moment you wrap the body in curly braces, though, you have to write return yourself.
Which one should you use? In modern code, arrow functions are very common for short, one-line jobs and callbacks. For named functions that do a page's main work, declarations are still popular. We'll see in the Intermediate series that arrow functions also behave differently when it comes to this; for now, feel free to use both.
Functions Are Values
In JavaScript a function is a value, just like a number or a string. You can assign it to a variable, put it inside an array, or pass it to another function as an argument. In the previous post, when we gave functions to forEach and sort, that's exactly what we did. A function that's handed to another function and called by it at the right moment is called a callback:
function calculate(a, b, operation) {
return operation(a, b);
}
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
console.log(calculate(6, 3, add)); // 9
console.log(calculate(6, 3, multiply)); // 18
console.log(calculate(6, 3, (a, b) => a - b)); // 3
calculate doesn't know what kind of operation it will perform; you give it the operation. It's like putting a capsule into a coffee machine: the machine stays the same, and what comes out changes with the capsule. On the last line we wrote the function directly as an argument, without even putting it in a variable. You'll run into this pattern constantly on the browser side, from event listeners to timers.
A Function's Own Space: Scope
Variables you define inside a function belong to that function; they aren't visible from outside. Think of the function as a room with a one-way mirror. From inside you can see out, but from outside you can't see in:
const taxRate = 0.2;
function addTax(price) {
const tax = price * taxRate;
return price + tax;
}
console.log(addTax(100)); // 120
console.log(typeof tax); // "undefined"
addTax could read the taxRate outside. But the tax it created inside didn't leak out; when the function finished, so did that variable. Parameters follow the same rule; they are the function's local variables too. This is the function version of the block scope we saw earlier. We'll dig into the scope chain and closures in the Intermediate series.
This rule is a safety net. Since every function has its own variables, using a variable named total in two different functions doesn't make them break each other.
Writing Good Functions
Writing a function is easy; writing functions that are easy to read, test and change is a matter of habit. A few rules take you a long way.
A function should do one thing. If you need the word "and" to name it, you probably need two functions. Three separate functions are always easier to manage than validateAndSaveAndNotify.
The name should start with a verb and say what it does: calculateTotal, formatPrice, getUserName. For functions that return true or false, starting with is, has or can is a nice convention: isValidEmail, hasDiscount. Someone reading the code should understand what a function does without looking inside it.
Keep the number of parameters small. Past three, the caller starts mixing up which position means what. And whenever you can, return a value instead of changing variables outside. A function that always gives the same output for the same input is much easier to predict.
Common Pitfalls
Calling the function instead of passing it
const showItem = (item) => console.log(`Item: ${item}`);
["pen", "notebook"].forEach(showItem); // right: we pass the function
// ["pen", "notebook"].forEach(showItem()); // wrong: we call it first and pass the result
If you add parentheses when passing a callback, the function runs immediately and forEach receives its return value, which here is undefined. The result is a TypeError. The rule is simple: no parentheses when you're handing over the machine, parentheses when you're pressing the button.
The forgotten return in an arrow with braces
const double = (n) => { n * 2 };
console.log(double(5)); // undefined
const makeUser = (name) => ({ name: name, active: true });
console.log(makeUser("Ada")); // { name: "Ada", active: true }
The moment you open curly braces, the implicit return is gone; either remove the braces or write return. The second case is a relative of this one. If you want to return an object directly from an arrow function, you have to wrap the object in regular parentheses; otherwise the engine thinks the curly braces are the function body. We'll go into objects in the next post.
Default values don't kick in for null
function greet(name = "guest") {
return `Hello ${name}`;
}
console.log(greet(undefined)); // "Hello guest"
console.log(greet(null)); // "Hello null"
A default parameter is used only when the argument is undefined. null, an empty string or 0 count as valid values and go in as they are.
A Small Experiment
★☆☆ Write a getMax function that takes two numbers and returns the larger one; first as a function declaration, then as a one-line arrow function. Confirm in the console that both give the same result.
★★☆ Predict before running: given const f = (x = 10) => x * 2;, what do f(), f(3), f(null) and f("4") return? For the last two, remember the coercion we saw in the post on operators.
★★☆ Write a formatPrice function that takes a price and returns it in the "₺1.234,50" format. The Intl.NumberFormat we saw earlier will come in handy. Then run every value in a price array through this function with forEach and print it.
★★★ Write a function called repeat(times, action): it should call the action function times times, passing the round number as an argument each time. The call repeat(3, (round) => console.log(`Round ${round}`)) should print three lines.
We learned how to break code into small, named pieces with functions. But real-world data is rarely a single number or string: a user's name, email and age come together. In the next post we move on to objects, which keep this kind of related data in one place.