Skip to content
C Codeloom
JavaScript

Functions in JavaScript: Declarations, Expressions, and Returns

A practical guide to JavaScript functions — declarations vs expressions, hoisting, parameters and arguments, default and rest parameters, return values, and functions as first-class values.

·9 min read · By Yash Kesharwani
Intermediate 11 min read

What you'll learn

  • The difference between function declarations and expressions
  • How hoisting actually works for each form
  • Parameters, arguments, default values, and rest parameters
  • Return values — and what happens when you omit return
  • Why functions are first-class values, and what that unlocks
  • Habits that keep functions small, named, and reusable

Prerequisites

A function is a named, reusable chunk of behaviour. In JavaScript, functions are also values — you can pass them around like numbers or strings, store them in arrays, return them from other functions, and assign them to object properties. That dual nature is what makes the language feel both small and expressive at the same time.

This post covers the syntax in full, then the habits that make functions worth reading.

Function declarations

The most familiar form. Use the function keyword, give it a name, list its parameters, and write its body in braces:

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

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

A declaration creates a binding with the function’s name in the enclosing scope. You can call it from anywhere in that scope — even from a line that appears before the declaration in source order:

console.log(double(5));    // 10  — works

function double(n) {
  return n * 2;
}

That’s hoisting at work. JavaScript moves function declarations to the top of their scope before executing the code, so the name is available everywhere inside it. This is the only form that’s fully hoisted.

Function expressions

A function expression assigns a function to a variable. Most often the function is anonymous:

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

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

The variable follows normal let/const rules — see Variables: let, const, var. The function is not available before the line that defines it:

console.log(square(5));    // ReferenceError

const square = function (n) {
  return n * n;
};

You can give the expression a name, which is useful in stack traces and for self-reference inside recursion:

const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);
};

console.log(factorial(5));    // 120

The outer name is factorial; the inner fact only exists inside the function body.

Which form should you use?

Most modern codebases prefer const name = function ... (or arrow functions, covered in the next post). The reason is predictability: a const-bound function exists from its line onward, just like any other variable, with no surprise hoisting.

Declarations remain useful for top-level helpers in a file where order doesn’t matter. Both forms produce the same kind of function object — pick by what reads best.

Parameters and arguments

The names listed in the function definition are parameters. The values you pass when calling it are arguments.

function rectangleArea(width, height) {   // parameters
  return width * height;
}

rectangleArea(3, 4);                       // arguments: 3 and 4

JavaScript does not check arity. Missing arguments become undefined; extra ones are quietly ignored:

function greet(name, mood) {
  return `${name} is ${mood}.`;
}

console.log(greet("Ada"));              // 'Ada is undefined.'
console.log(greet("Ada", "happy", 99)); // 'Ada is happy.'

This flexibility is a double-edged sword. Lean on default parameters and clear names so the right call shape is obvious.

Default parameters

Provide a fallback value with =:

function greet(name, mood = "fine") {
  return `${name} is ${mood}.`;
}

console.log(greet("Ada"));            // 'Ada is fine.'
console.log(greet("Ada", "tired"));   // 'Ada is tired.'

The default is evaluated each time the function is called, not once when it’s defined. That makes this safe:

function appendTo(item, list = []) {
  list.push(item);
  return list;
}

console.log(appendTo(1));    // [1]
console.log(appendTo(2));    // [2]  — fresh array each call

Defaults can reference previous parameters:

function box(width, height = width) {
  return { width, height };
}

console.log(box(5));      // { width: 5, height: 5 }
console.log(box(5, 8));   // { width: 5, height: 8 }

Only undefined triggers a default. Explicit null does not:

greet("Ada", undefined);   // 'Ada is fine.'   — default used
greet("Ada", null);        // 'Ada is null.'   — default skipped

Rest parameters

Use ... in the parameter list to collect any number of trailing arguments into a real array:

function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(1, 2, 3, 4, 5));  // 15
console.log(sum());               // 0

Rest can follow fixed parameters but must be last:

function tag(label, ...items) {
  return `${label}: ${items.join(", ")}`;
}

console.log(tag("fruits", "apple", "banana", "cherry"));
// 'fruits: apple, banana, cherry'

Rest is the modern replacement for the old arguments object. Prefer it — it’s a real array, so map, filter, and friends work without conversion. See Arrays for why that matters.

Return values

return ends the function and produces a value:

function abs(n) {
  if (n < 0) return -n;
  return n;
}

console.log(abs(-7));    // 7

A function with no return, or a bare return;, evaluates to undefined:

function log(msg) {
  console.log(msg);
}

const result = log("hi");
console.log(result);    // undefined

That’s the natural placeholder for a function that does work but produces no value (logging, mutating, drawing). To return multiple values, return an object or array — and let the caller destructure:

function divmod(a, b) {
  return { quotient: Math.floor(a / b), remainder: a % b };
}

const { quotient, remainder } = divmod(17, 5);
console.log(quotient, remainder);    // 3 2

Early returns flatten branches

When a function has several “this case handled, done” branches, return as soon as you reach each one. The result has no nesting:

function classify(score) {
  if (score < 0 || score > 100) return "invalid";
  if (score < 50) return "fail";
  if (score < 70) return "pass";
  if (score < 85) return "merit";
  return "distinction";
}

Compare that to a deeply nested if/else version — same logic, much harder to read.

Try it yourself. Write a function priceWithTax(price, rate = 0.1, currency = "USD") that returns a string like "USD 11.00". Call it with one argument, two arguments, and all three. Then write a second version that takes a single options object and destructures it.

Functions are first-class values

This is the part that changes how you write JavaScript. A function is an object. You can store it, pass it, and return it like any other value.

Store in a variable

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

console.log(greet("Ada"));    // 'Hello, Ada!'

Pass as an argument

The callback-based array methods you saw in the Arrays post work because of this:

const nums = [1, 2, 3, 4];

function double(n) {
  return n * 2;
}

console.log(nums.map(double));   // [2, 4, 6, 8]

Return from another function

A function that builds a function is a higher-order function:

function multiplier(factor) {
  return function (n) {
    return n * factor;
  };
}

const triple = multiplier(3);
console.log(triple(5));    // 15
console.log(triple(10));   // 30

The returned function remembers factor even after multiplier has finished. That memory is called a closure — one of the most useful concepts in the language.

Store in objects and arrays

const calculator = {
  add(a, b) { return a + b; },
  subtract(a, b) { return a - b; },
};

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

A function stored on an object is called a method. The shorthand above is equivalent to writing add: function (a, b) { ... }.

Naming and signature habits

Functions live or die by how their signature reads at the call site.

  • Verbs for actions, nouns for getters: saveUser, formatDate, currentTime.
  • Booleans get is/has/can: isAdmin, hasAccess, canEdit.
  • Two arguments max, ideally. Beyond that, take an options object.
  • No surprise side effects. A function called validate shouldn’t also mutate.

The options-object pattern uses destructuring in the parameter list:

function fetchUser({ id, includePosts = false, includeFriends = false } = {}) {
  return { id, includePosts, includeFriends };
}

console.log(fetchUser({ id: 1 }));
// { id: 1, includePosts: false, includeFriends: false }

console.log(fetchUser({ id: 2, includePosts: true }));
// { id: 2, includePosts: true, includeFriends: false }

The trailing = {} lets you call fetchUser() with no arguments at all without a TypeError when it tries to destructure undefined.

Try it yourself. Write a range(start, end, step = 1) function that returns an array of numbers from start (inclusive) to end (exclusive), stepping by step. Use rest or default parameters as appropriate. Then call it as range(0, 10) and range(0, 10, 2).

A small worked example

A miniature pricing engine that uses defaults, rest parameters, and higher-order functions together.

function applyDiscount(rate) {
  return function (price) {
    return price * (1 - rate);
  };
}

function total(...prices) {
  return prices.reduce((acc, p) => acc + p, 0);
}

function formatPrice(amount, currency = "USD") {
  return `${currency} ${amount.toFixed(2)}`;
}

const memberDiscount = applyDiscount(0.15);
const prices = [49.99, 19.99, 9.5];

const discounted = prices.map(memberDiscount);
const grandTotal = total(...discounted);

console.log(formatPrice(grandTotal));    // 'USD 67.55'

Every concept in this post appears in those lines: a function returning a function, a function as a callback, rest parameters, default parameters, and named methods all working together. This is the standard texture of real JavaScript.

Recap

You now know:

  • Declarations are hoisted; expressions assigned to const are not
  • Parameters are the names; arguments are the values passed
  • Missing arguments become undefined; extras are ignored
  • Default parameters fire only on undefined, never on null
  • Rest parameters (...nums) collect trailing arguments into an array
  • return ends the function; missing return yields undefined
  • Early returns flatten branchy code
  • Functions are first-class values — pass them, return them, store them

Next steps

The next post is about a tighter syntax for writing functions — arrow functions — and the subtle but important difference in how they handle this.

→ Next: Arrow Functions and this in JavaScript

Questions or feedback? Email codeloomdevv@gmail.com.