Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

JavaScript Functions: Declarations, Arrows, Defaults, and First-Class Power

If you learn one thing about JavaScript deeply, make it functions. Not because the syntax is hard โ€” it isnโ€™t โ€” but because JS is built on a premise many languages only bolted on later: functions are ordinary values. Theyโ€™re assigned to variables, passed as arguments, returned from other functions, and stored in arrays and objects, exactly like numbers and strings.

Every distinctive JS pattern โ€” callbacks, event handlers, array methods, promises, middleware, React components โ€” is this one premise wearing different outfits. This guide covers the syntax forms and their real differences, parameter machinery, and the higher-order patterns that make JS code look the way it does.


Three Ways to Write a Function

// 1. Function declaration
function add(a, b) {
return a + b;
}
// 2. Function expression (assigned to a variable)
const subtract = function (a, b) {
return a - b;
};
// 3. Arrow function (ES2015 โ€” now the workhorse)
const multiply = (a, b) => a * b;

All three create callable function objects. The differences that actually matter:

Hoisting. Declarations are fully hoisted โ€” callable anywhere in their scope, even above the definition, which lets you organize files top-down (main flow first, helpers below). Expressions and arrows live under variable rules: callable only after the assignment line.

this behavior. The big one: arrows donโ€™t have their own this โ€” they inherit it from the surrounding scope, while function functions get this determined at call time. This single difference is why callbacks inside classes/objects historically broke, and why arrows fixed them. It deserves โ€” and gets โ€” a whole guide; until then: arrows for callbacks is a safe default.

Arrows are expressions all the way down, with syntax that shrinks to fit:

const square = (x) => x * x; // one param โ€” parens optional: x => x * x
const rand = () => Math.random(); // no params โ€” parens required
const clamp = (x, lo, hi) => // multiple params
Math.min(Math.max(x, lo), hi);
const bigger = (x) => { // block body โ€” 'return' required again
const doubled = x * 2;
return doubled + 1;
};
const makeUser = (name) => ({ name, active: true }); // returning an object literal:
// wrap in parens, or the braces
// parse as a block!

That last one is a real-world gotcha: => { name: name } is a block containing a label, silently returning undefined. The wrapping parens => ({ ... }) are mandatory for object-literal bodies.

Which to use? A sane modern default: function declarations for top-level named operations (hoisting + clearer stack traces + syntactic weight matching their importance), arrows for callbacks and small inline logic. Teams vary; consistency beats doctrine.


Parameters: Defaults, Rest, and the Absence of Overloading

JavaScript functions accept any number of arguments regardless of the parameter list โ€” missing ones are undefined, extras are ignored. No compile error, no overloading. The modern toolkit turns this looseness into expressiveness:

Default parameters โ€” evaluated per call, only when the argument is undefined:

function connect(host, port = 5432, retries = 3) {
// ...
}
connect("db.local"); // port 5432, retries 3
connect("db.local", 6543); // retries 3
connect("db.local", undefined, 5); // explicit undefined triggers the default
connect("db.local", null, 5); // null does NOT โ€” port is null. ?? mindset applies.

Rest parameters โ€” gather remaining arguments into a real array:

function logAll(prefix, ...items) { // items is a genuine Array
for (const item of items) console.log(prefix, item);
}
logAll(">>", "a", "b", "c");

(If you meet the older arguments object in legacy code: itโ€™s the pre-2015 version of this โ€” array-like, not an array, and absent in arrows. Rest parameters replaced it.)

Destructured parameters โ€” the modern answer to โ€œtoo many positional argumentsโ€:

function createUser({ name, email, role = "viewer", active = true }) {
// ...
}
createUser({ name: "Ada", email: "ada@x.com" }); // self-documenting call site

Once a function needs more than two or three arguments โ€” especially same-typed ones that are easy to swap โ€” an options object with destructuring is strictly better: named at the call site, order-independent, defaults per field. This pattern is ubiquitous in modern APIs and covered mechanically in destructuring.

One discipline the language wonโ€™t enforce for you: functions should return consistent types. A function that returns a number usually but false on failure (an old JS idiom) pushes type-checking burdens onto every caller. Return null for absence, throw for errors, or return a result object โ€” pick one contract and keep it.


Functions as Values: The Load-Bearing Idea

Because functions are values, you can build with them:

// stored in data structures
const operations = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
};
operations.add(2, 3); // 5 โ€” a lookup table of behavior
// passed as arguments โ€” the callback pattern
[3, 1, 2].sort((a, b) => a - b); // sort takes a comparison FUNCTION
button.addEventListener("click", handleClick); // events take handler FUNCTIONS
// returned from functions โ€” function factories
const makeGreeter = (greeting) => (name) => `${greeting}, ${name}!`;
const hello = makeGreeter("Hello");
const namaste = makeGreeter("Namaste");
hello("Ada"); // "Hello, Ada!"
namaste("Arun"); // "Namaste, Arun!"

Functions that take or return functions are called higher-order functions, and theyโ€™re not an advanced topic in JS โ€” theyโ€™re Tuesday. The three patterns above cover most usage:

  1. Behavior tables replace switch statements: dispatch by key, extend by adding an entry.
  2. Callbacks parameterize an algorithm with a step you supply โ€” the JS pattern. sort knows how to sort; you tell it how to compare. map knows how to traverse; you tell it how to transform. Every event API, array method, and async interface works this way.
  3. Factories produce configured functions โ€” possible because returned functions remember the variables they were created with (greeting above). That memory is a closure, important enough to be its own chapter; here, just notice it works.

A practical corollary many beginners miss โ€” pass the function, donโ€™t call it:

button.addEventListener("click", handleClick()); // BUG: calls now, passes the result
button.addEventListener("click", handleClick); // right: passes the function itself
button.addEventListener("click", () => handleClick(id)); // right: wrap when you need args

The bare name is the value; parentheses invoke. This one confusion produces a steady stream of โ€œmy handler runs immediately on page loadโ€ questions.


Pure Functions and Side Effects: A Vocabulary Worth Adopting Early

A pure function computes its return value from its inputs and does nothing else โ€” no mutation of outside state, no I/O, same inputs always same output:

const withTax = (paise, rate) => Math.round(paise * (1 + rate)); // pure
let total = 0;
const addToTotal = (x) => { total += x; }; // impure: mutates outside

Purity isnโ€™t a purity test โ€” real programs need side effects (rendering, network, storage). The engineering value is segregation: keep calculation pure and push effects to the edges. Pure functions are trivially testable (call, assert โ€” no setup, no mocks), safe to reorder and cache, and immune to spooky action at a distance. Frameworks have converged on this: Reactโ€™s render logic, reducers, and most โ€œbusiness logicโ€ layers are expected to be pure, with effects quarantined in designated places. Building the habit now โ€” โ€œcould this function be pure?โ€ โ€” pays compounding dividends.

Related habit: name functions after what they return or do (formatPrice, isEligible, fetchUser โ€” verb or predicate), keep them small enough to name honestly, and extract when a name would need โ€œand.โ€


IIFEs and Immediate Invocation, Briefly

Legacy code is full of this shape:

(function () {
var private = "hidden";
// ... whole libraries lived inside these
})();

An IIFE (immediately-invoked function expression) โ€” define and call in one breath โ€” was the pre-2015 way to create a private scope, since var had no block scope and there were no modules. Today, block scope and ES modules cover those needs, and the surviving modern use is small: initializing a const that needs multi-statement logic, now often written as an arrow: const config = (() => { ...; return result; })();. Recognize the pattern; rarely write it.


Frequently Asked Questions

When must I NOT use an arrow function? When you need a dynamic this: object methods defined with arrows canโ€™t see their own object, and DOM handlers that rely on this being the element break. Also: arrows canโ€™t be constructors and have no arguments. Full rules in the this guide โ€” the summary is arrows for callbacks, method shorthand for methods.

Are there truly private variables without classes? Yes โ€” closure over function scope is the original privacy mechanism (makeCounter-style factories), and itโ€™s airtight. Classes later added #private fields; both are legitimate. The closures chapter builds this out.

Whatโ€™s the difference between parameters and arguments? Parameters are the names in the definition; arguments are the values at the call. Sloppy usage is universal and harmless, but the distinction makes the defaults/rest rules crisper.

Can I overload by arity like Java? No โ€” one function per name; later declarations overwrite. The idioms instead: default parameters, options objects, or explicit branching on typeof/argument count inside one function. The options-object pattern scales best and dominates modern APIs.

How many parameters is too many? By the time you hit three โ€” especially with same-typed values (resize(300, 400, 200) โ€” which is which?) โ€” switch to a destructured options object. Future readers of call sites will thank you.


Naming and Shaping Functions: A Review-Room Consensus

A few conventions that recur in strong codebases, worth adopting before habits calcify:

Predicates read as questions: isEligible, hasAccess, canRetry โ€” and they return actual booleans (return count > 0, not return count && flag which returns a number-or-boolean cocktail). Actions read as verb-phrases (sendInvoice, buildReport); getters read as nouns or get-phrases (totalPaise, getConfig). A function whose honest name would contain โ€œandโ€ (validateAndSave) is two functions in a trench coat.

Arity discipline: zero and one-argument functions compose best (notice how pipe in the self-check only works because each step takes one value). When a function grows optional knobs, they go in a trailing options object, never as a fourth positional boolean โ€” fetchUsers(true, false, true) is unreviewable; fetchUsers({ includeInactive: true }) documents itself at every call site.

Early returns beat arrow-shaped nesting: guard clauses first (if (!user) return null;), happy path unindented at the bottom. JSโ€™s truthiness tools make guards terse โ€” use that.

Default to returning, not mutating โ€” the pure-function principle applied at the smallest scale: withDiscount(order) returning a new object beats applyDiscount(order) editing in place, because the caller controls what changes and the spread idioms make copies cheap to express.

None of these are enforced by the language โ€” thatโ€™s the point. JS gives functions maximal freedom; teams that thrive impose the structure themselves, consistently.

Self-Check

// 1. What prints?
const f = (x = 2, y = x * 2) => x + y;
console.log(f()); // ?
console.log(f(5)); // ?
// 2. What's wrong here?
const getConfig = () => { env: "prod" };
console.log(getConfig()); // ?
// 3. Spot the bug:
setTimeout(cleanup(), 1000);
// 4. What does this build?
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const shout = pipe((s) => s.trim(), (s) => s.toUpperCase(), (s) => s + "!");
console.log(shout(" hey ")); // ?

Answers: (1) 6 and 15 โ€” later defaults can use earlier parameters. (2) prints undefined โ€” the braces parse as a block, env: as a label; needs => ({ env: "prod" }). (3) cleanup runs immediately and its return value (probably undefined) is scheduled; drop the parens or wrap in an arrow. (4) "HEY!" โ€” pipe is a higher-order factory composing functions left to right; if you followed it, youโ€™re reading real-world functional JS already.

Next up, the concept that makes function factories and private state possible โ€” and the most asked-about topic in JavaScript interviews: Closures.