Skip to main content

How to Use Function Expressions in JavaScript

In the previous chapter, you learned to create functions using declarations. Function expressions are the second way to create functions in JavaScript, and they unlock an entirely different way of thinking about code. When you write a function expression, you are treating a function as a value, something that can be assigned to a variable, passed around, and created on the fly, just like a number or a string.

This distinction is not just syntactic. Function expressions behave differently from declarations in terms of hoisting, scope, and flexibility. They are the foundation of callbacks, event handlers, closures, and virtually every modern JavaScript pattern. This guide focuses specifically on function expressions, exploring their syntax, their unique characteristics compared to declarations, and the powerful patterns they enable.

Function Declaration vs. Function Expression

Understanding the distinction between these two forms is essential because they look similar but have fundamentally different behaviors.

Function Declaration

A function declaration is a statement. It stands on its own and declares a named function:

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

The function keyword appears at the beginning of the statement. This is how JavaScript recognizes it as a declaration.

Function Expression

A function expression creates a function as part of a larger expression. The most common form assigns the function to a variable:

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

Here, function(name) { ... } is an expression that produces a function value. That value is then assigned to the variable greet. The semicolon at the end is required because this is a variable assignment statement, just like const x = 5;.

How JavaScript Tells Them Apart

JavaScript decides whether a function keyword creates a declaration or an expression based on context:

// Declaration: 'function' is at the start of a statement
function add(a, b) { return a + b; }

// Expression: 'function' appears inside an assignment
const add = function(a, b) { return a + b; };

// Expression: 'function' appears inside parentheses
(function() { console.log("IIFE"); })();

// Expression: 'function' appears as an argument
setTimeout(function() { console.log("delayed"); }, 1000);

// Expression: 'function' appears after an operator
const negate = -function() { return -1; }();

// Expression: 'function' appears in an array
const fns = [function() { return 1; }, function() { return 2; }];

// Expression: 'function' appears as a return value
function maker() {
return function() { return 42; };
}

The rule is simple: if function is the very first token in a statement, it is a declaration. Anywhere else, it is an expression.

Named Function Expressions

Function expressions can optionally include a name. This creates a Named Function Expression (NFE):

// Anonymous function expression
const multiply = function(a, b) {
return a * b;
};

// Named function expression
const multiply = function multiplyNumbers(a, b) {
return a * b;
};

The name of an NFE (multiplyNumbers in this case) has two important properties:

  1. It is only accessible inside the function itself
  2. It appears in stack traces, making debugging easier
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // 'fact' works inside the function
};

console.log(factorial(5)); // 120
console.log(typeof fact); // "undefined" ()'fact' is NOT accessible outside)

Why the Internal Name Matters

The internal name of an NFE provides a reliable self-reference that survives reassignment:

// Without NFE: reassignment breaks recursion
let countDown = function(n) {
if (n <= 0) return;
console.log(n);
countDown(n - 1); // References the variable, not the function
};

const original = countDown;
countDown = null; // Variable reassigned

original(3); // TypeError: countDown is not a function
// The recursive call tries to use the variable 'countDown', which is now null
// With NFE: the internal name always works
let countDown = function countdown(n) {
if (n <= 0) return;
console.log(n);
countdown(n - 1); // References the function's own name, not the variable
};

const original = countDown;
countDown = null; // Variable reassigned (doesn't matter)

original(3); // 3, 2, 1 (works! 'countdown' is the function's own name)
tip

Use named function expressions when the function is recursive or when you want clear names in error stack traces. For simple callbacks, anonymous expressions or arrow functions are fine because modern engines infer names from their context.

Functions as Values (First-Class Functions)

The fundamental concept behind function expressions is that functions in JavaScript are first-class citizens. This means a function is a value, no different from a number, a string, or an object. You can do anything with a function value that you can do with any other value.

Functions Are Just Values

When you write a function expression, you are creating a value and storing it:

// A number value stored in a variable
const age = 30;

// A string value stored in a variable
const name = "Alice";

// A function value stored in a variable
const greet = function(person) {
return `Hello, ${person}!`;
};

// All three are just values
console.log(typeof age); // "number"
console.log(typeof name); // "string"
console.log(typeof greet); // "function"

Passing Functions Around

Because functions are values, you can pass them to other variables, just like copying a number:

function sayHello() {
return "Hello!";
}

// Assign the function (NOT calling it, no parentheses)
const greeting = sayHello;

console.log(sayHello()); // "Hello!"
console.log(greeting()); // "Hello!" (same function, different variable)

// Both variables point to the same function
console.log(sayHello === greeting); // true
warning

The distinction between sayHello and sayHello() is critical:

  • sayHello is the function value itself (a reference to the function)
  • sayHello() calls the function and evaluates to its return value
function getNumber() { return 42; }

console.log(getNumber); // [Function: getNumber] (the function object)
console.log(getNumber()); // 42 (the return value)

const ref = getNumber; // Store the function
const val = getNumber(); // Store the return value (42)

Functions in Data Structures

Functions can be stored in arrays, objects, Maps, and any other data structure:

// Functions in an array
const pipeline = [
function(str) { return str.trim(); },
function(str) { return str.toLowerCase(); },
function(str) { return str.replace(/\s+/g, "-"); },
];

// Run a value through the pipeline
let result = " Hello Beautiful World ";
for (let transform of pipeline) {
result = transform(result);
}
console.log(result); // "hello-beautiful-world"
// Functions as object properties (methods via expressions)
const validators = {
isEmail: function(str) {
return str.includes("@") && str.includes(".");
},
isPhone: function(str) {
return /^\d{10}$/.test(str);
},
isNotEmpty: function(str) {
return str.trim().length > 0;
},
};

console.log(validators.isEmail("alice@example.com")); // true
console.log(validators.isPhone("1234567890")); // true
console.log(validators.isNotEmpty(" ")); // false

Functions Returning Functions

A function can create and return new functions. This is a pattern called a factory function or higher-order function:

function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}

const sayHello = createGreeter("Hello");
const sayGoodbye = createGreeter("Goodbye");
const sayHola = createGreeter("Hola");

console.log(sayHello("Alice")); // "Hello, Alice!"
console.log(sayGoodbye("Bob")); // "Goodbye, Bob!"
console.log(sayHola("Carlos")); // "Hola, Carlos!"

Each call to createGreeter produces a new function with the greeting value baked in. The returned function "remembers" the greeting from its creation context. This is a closure, covered in depth later.

Practical Factory: Creating Validators

function createRangeValidator(min, max) {
return function(value) {
if (value < min) return `Value must be at least ${min}`;
if (value > max) return `Value must be at most ${max}`;
return null; // null means valid
};
}

const validateAge = createRangeValidator(0, 150);
const validateScore = createRangeValidator(0, 100);
const validateTemperature = createRangeValidator(-50, 60);

console.log(validateAge(25)); // null (valid)
console.log(validateAge(200)); // "Value must be at most 150"
console.log(validateScore(-5)); // "Value must be at least 0"
console.log(validateTemperature(35)); // null (valid)

Callback Functions: Passing Functions as Arguments

A callback is a function expression (or reference) passed as an argument to another function. The receiving function decides when and how to call it. Callbacks are the single most important pattern that function expressions enable.

The Concept

// 'operation' is a callback (we don't know what it does until it's called)
function applyToNumbers(a, b, operation) {
return operation(a, b);
}

// Pass different callbacks for different behavior
console.log(applyToNumbers(10, 5, function(a, b) { return a + b; })); // 15
console.log(applyToNumbers(10, 5, function(a, b) { return a - b; })); // 5
console.log(applyToNumbers(10, 5, function(a, b) { return a * b; })); // 50
console.log(applyToNumbers(10, 5, function(a, b) { return a ** b; })); // 100000

The function applyToNumbers does not know what operation to perform. It delegates that decision to the caller by accepting a callback. This is the core of abstraction: separating the general structure from the specific behavior.

Callbacks with Array Methods

JavaScript's built-in array methods are designed around callbacks. Each method defines the "how" (iterating, filtering, mapping), and you provide the "what" (the specific logic):

const products = [
{ name: "Laptop", price: 999, category: "Electronics" },
{ name: "Book", price: 15, category: "Education" },
{ name: "Phone", price: 699, category: "Electronics" },
{ name: "Pen", price: 2, category: "Office" },
{ name: "Tablet", price: 449, category: "Electronics" },
];

// filter: callback decides which items to keep
const expensive = products.filter(function(product) {
return product.price > 100;
});
console.log(expensive.length); // 3

// map: callback decides how to transform each item
const names = products.map(function(product) {
return product.name;
});
console.log(names); // ["Laptop", "Book", "Phone", "Pen", "Tablet"]

// find: callback decides which item to return
const firstElectronic = products.find(function(product) {
return product.category === "Electronics";
});
console.log(firstElectronic.name); // "Laptop"

// sort: callback decides the ordering
const byPrice = [...products].sort(function(a, b) {
return a.price - b.price;
});
console.log(byPrice.map(p => p.name)); // ["Pen", "Book", "Tablet", "Phone", "Laptop"]

// every: callback tests if ALL items pass
const allAffordable = products.every(function(product) {
return product.price < 500;
});
console.log(allAffordable); // false

// some: callback tests if ANY item passes
const hasElectronics = products.some(function(product) {
return product.category === "Electronics";
});
console.log(hasElectronics); // true

Writing Functions That Accept Callbacks

Creating your own functions that accept callbacks is straightforward. You simply call the parameter as a function:

function fetchData(url, onSuccess, onError) {
// Simulating an async operation
let success = Math.random() > 0.3;

if (success) {
let data = { id: 1, name: "Sample Data" };
onSuccess(data);
} else {
onError("Network error: connection refused");
}
}

// Using the function with callback expressions
fetchData(
"https://api.example.com/users",
function(data) {
console.log("Success:", data);
},
function(error) {
console.error("Error:", error);
}
);

Callbacks for Customizable Behavior

function repeatAction(times, action) {
for (let i = 0; i < times; i++) {
action(i);
}
}

// Same structure, different behavior based on callback
repeatAction(5, function(i) {
console.log("*".repeat(i + 1));
});
// *
// **
// ***
// ****
// *****

repeatAction(3, function(i) {
console.log(`Step ${i + 1} of 3`);
});
// Step 1 of 3
// Step 2 of 3
// Step 3 of 3

Composing Callbacks: Building Complex Logic from Simple Parts

function compose(fn1, fn2) {
return function(value) {
return fn2(fn1(value));
};
}

const trim = function(str) { return str.trim(); };
const lower = function(str) { return str.toLowerCase(); };
const exclaim = function(str) { return str + "!"; };

const cleanAndExclaim = compose(compose(trim, lower), exclaim);

console.log(cleanAndExclaim(" HELLO WORLD ")); // "hello world!"

Event Handlers Are Callbacks

In browser JavaScript, event handlers are callbacks that the browser calls when events occur:

// The function expression is a callback that the browser will invoke
document.getElementById("submitBtn").addEventListener("click", function(event) {
event.preventDefault();
console.log("Form submitted!");
});

// Timer callbacks
setTimeout(function() {
console.log("This runs after 2 seconds");
}, 2000);

setInterval(function() {
console.log("This runs every 3 seconds");
}, 3000);

The browser holds a reference to your function expression and calls it later when the event fires. You never call these functions yourself.

Anonymous Functions

An anonymous function is a function expression without a name. Most of the function expressions in the examples above are anonymous.

Named vs. Anonymous

// Anonymous: no name between 'function' and '()'
const add = function(a, b) {
return a + b;
};

// Named: 'addNumbers' between 'function' and '()'
const add = function addNumbers(a, b) {
return a + b;
};

Where Anonymous Functions Make Sense

Anonymous functions are appropriate when the function is short, used once, and its purpose is clear from context:

// Good use of anonymous functions: short, inline, purpose is clear
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];

const sorted = numbers.sort(function(a, b) { return a - b; });
const evens = numbers.filter(function(n) { return n % 2 === 0; });
const doubled = numbers.map(function(n) { return n * 2; });

// The method name (sort, filter, map) provides enough context
// The callback is simple enough that a name adds no value

Where Named Functions Are Better

Use named functions when the function is complex, reused, or when you need clear stack traces:

// BAD: complex anonymous function is hard to debug
app.get("/users", function(req, res) {
// 50 lines of complex logic
// If an error occurs, the stack trace shows "anonymous"
});

// GOOD: named function is easier to debug and reuse
app.get("/users", function handleGetUsers(req, res) {
// 50 lines of complex logic
// Stack trace shows "handleGetUsers" (easy to find)
});

// EVEN BETTER: extract to a separate named function
function handleGetUsers(req, res) {
// 50 lines of complex logic
}
app.get("/users", handleGetUsers);

Name Inference in Modern JavaScript

Modern JavaScript engines infer names from the assignment context:

// Name is inferred from the variable
const greet = function(name) {
return `Hello, ${name}`;
};
console.log(greet.name); // "greet"
// Name is inferred from the object property
const utils = {
formatDate: function(date) {
return date.toLocaleDateString();
}
};
console.log(utils.formatDate.name); // "formatDate"
// Name is inferred from default parameter
function setup(callback = function() { return "default"; }) {
console.log(callback.name); // "callback"
}
// No inference for inline callbacks
[1, 2, 3].forEach(function(n) {
// This function's name would be empty or "anonymous"
});

Name inference makes anonymous functions more debuggable than they used to be, but an explicit name is still clearer in stack traces.

Pattern: Choosing Between Anonymous and Named

// Short, simple, single-use → anonymous (or arrow function)
const evens = numbers.filter(function(n) { return n % 2 === 0; });
setTimeout(function() { console.log("done"); }, 100);

// Complex logic → named expression
const validate = function validateUserInput(input) {
// Multiple validation steps...
// Name appears in stack traces
};

// Reused in multiple places → function declaration or named variable
function isPositive(n) { return n > 0; }
const positives = numbers.filter(isPositive);
const hasPositive = numbers.some(isPositive);

Hoisting Differences: Declarations vs. Expressions

This is the most practically important difference between function declarations and function expressions.

Function Declarations: Fully Hoisted

Function declarations are processed before any code executes. The entire function (name and body) is available throughout its scope:

// Works! Declaration is hoisted above this call
console.log(add(3, 4)); // 7

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

This is equivalent to:

// What JavaScript actually sees:
function add(a, b) { // Entire function hoisted to the top
return a + b;
}

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

Function Expressions: Follow Variable Hoisting Rules

Function expressions are assigned to variables, so they follow the hoisting behavior of whatever keyword (var, let, or const) is used.

With const (recommended):

console.log(add);       // ReferenceError: Cannot access 'add' before initialization
console.log(add(3, 4)); // Never reached

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

The variable add is in the Temporal Dead Zone until the const declaration is reached.

With let:

console.log(add);      // ReferenceError: Cannot access 'add' before initialization

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

Same behavior as const: TDZ error.

With var:

console.log(typeof add);  // "undefined" (variable exists but is undefined)
console.log(add(3, 4)); // TypeError: add is not a function

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

With var, the variable declaration is hoisted and initialized as undefined. Calling undefined(3, 4) throws a TypeError.

Why This Difference Matters in Practice

Order-dependent code with expressions:

// This breaks! process() tries to use helpers not yet defined
process();

const process = function() {
const data = loadData(); // ReferenceError: loadData is not defined
const result = transform(data);
display(result);
};

const loadData = function() { return [1, 2, 3]; };
const transform = function(data) { return data.map(n => n * 2); };
const display = function(result) { console.log(result); };

Order-independent code with declarations:

// This works! All declarations are hoisted
process();

function process() {
const data = loadData(); // Works (hoisted)
const result = transform(data); // Works (hoisted)
display(result); // Works (hoisted)
}

function loadData() { return [1, 2, 3]; }
function transform(data) { return data.map(n => n * 2); }
function display(result) { console.log(result); }

Conditional Function Creation

Function expressions are the correct way to create functions based on conditions, because they follow normal execution flow:

let formatCurrency;

if (locale === "US") {
formatCurrency = function(amount) {
return `$${amount.toFixed(2)}`;
};
} else if (locale === "EU") {
formatCurrency = function(amount) {
return `${amount.toFixed(2)}`;
};
} else {
formatCurrency = function(amount) {
return `${amount.toFixed(2)} units`;
};
}

console.log(formatCurrency(99.5));

Function declarations inside if blocks have inconsistent behavior across different JavaScript environments and strict/non-strict modes:

// AVOID: function declaration inside a block
// Behavior is inconsistent across environments
if (true) {
function helper() { return "A"; }
} else {
function helper() { return "B"; }
}
// In some engines: helper() returns "B" (both declarations hoisted, second wins)
// In strict mode: helper is block-scoped
// The behavior is unreliable (use expressions instead)

When Each Is Appropriate

// Use DECLARATIONS for:
// - Standalone, reusable utility functions
// - Functions that define the main structure of your program
// - When you want functions to be available throughout the scope

function calculateTax(amount, rate) {
return amount * rate;
}

function formatPrice(amount) {
return `$${amount.toFixed(2)}`;
}

// Use EXPRESSIONS for:
// - Callbacks and inline functions
// - Conditional function creation
// - Assigning functions to object properties
// - When you want to prevent calling before definition
// - When you need the function as a value to pass around

const onSubmit = function(event) {
event.preventDefault();
processForm();
};

const strategies = {
add: function(a, b) { return a + b; },
multiply: function(a, b) { return a * b; },
};

Immediately Invoked Function Expressions (IIFE)

An IIFE is a function expression that is created and called in the same statement. It executes immediately, runs once, and the function itself cannot be called again.

Syntax and Mechanics

The key challenge is making JavaScript treat the function keyword as an expression rather than a declaration. Parentheses accomplish this:

// Step 1: A normal function expression
function() { console.log("Hi"); }
// SyntaxError! JS sees 'function' at the start and expects a declaration name

// Step 2: Wrap in parentheses to force expression context
(function() { console.log("Hi"); })
// Valid expression, but doesn't execute

// Step 3: Add () to invoke immediately
(function() { console.log("Hi"); })();
// "Hi" (creates the function, calls it, done)

IIFE with Parameters and Return Values

// Passing arguments to an IIFE
(function(name, age) {
console.log(`${name} is ${age} years old`);
})("Alice", 30);
// "Alice is 30 years old"

// Capturing the return value
const result = (function(a, b) {
return a + b;
})(10, 20);

console.log(result); // 30

The Purpose: Creating Private Scope

Before let and const, var was the only option for declaring variables, and var is function-scoped. The only way to create a private scope was with a function, and IIFE provided an anonymous scope wrapper:

// The problem: var pollutes the scope
var counter = 0;
var maxRetries = 3;
// These variables are visible to ALL other code in the same scope
console.log(typeof counter); // "number"
console.log(typeof maxRetries); // "number"
// The IIFE solution: private scope
(function() {
var counter = 0;
var maxRetries = 3;
// These variables exist only inside the IIFE
// No other code can see or modify them
})();

console.log(typeof counter); // "undefined"
console.log(typeof maxRetries); // "undefined"

The Module Pattern

IIFE enabled the module pattern, the standard way to create encapsulated modules before ES6:

const UserModule = (function() {
// Private state
let users = [];
let nextId = 1;

// Private function
function validate(user) {
return user.name && user.name.length > 0;
}

// Public API (returned object)
return {
add: function(name) {
let user = { id: nextId++, name: name };
if (validate(user)) {
users.push(user);
return user;
}
return null;
},
getAll: function() {
return [...users]; // Return a copy to prevent external modification
},
count: function() {
return users.length;
}
};
})();

// Using the module
UserModule.add("Alice");
UserModule.add("Bob");
console.log(UserModule.count()); // 2
console.log(UserModule.getAll()); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]

// Private members are inaccessible
console.log(typeof users); // "undefined"
console.log(typeof validate); // "undefined"
console.log(UserModule.users); // undefined

IIFE Variations

// Classic form
(function() { /* ... */ })();

// Alternative parenthesization
(function() { /* ... */ }());

// Arrow function IIFE (modern)
(() => { /* ... */ })();

// Async IIFE
(async function() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
})();

// Async arrow IIFE
(async () => {
const data = await fetchData();
processData(data);
})();

// IIFE with unary operator (avoids parentheses)
!function() { /* ... */ }();
void function() { /* ... */ }();

IIFE for Isolating Loop Variables (Pre-ES6)

Before let, IIFE was the fix for the classic closure-in-a-loop bug:

// BUG with var: all callbacks share the same i
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // Always 5 (the loop finished before timeouts fire)
}, 100);
}
// Output: 5, 5, 5, 5, 5
// IIFE FIX: each iteration captures its own copy of i
for (var i = 0; i < 5; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI); // Each callback has its own capturedI
}, 100);
})(i);
}
// Output: 0, 1, 2, 3, 4
// MODERN FIX: just use let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // let creates a new i for each iteration
}, 100);
}
// Output: 0, 1, 2, 3, 4

IIFE in Modern JavaScript

With let, const, block scope, and ES modules, the original motivations for IIFE have largely been addressed:

// IIFE for scope (old)
(function() {
var secret = "hidden";
})();
// Block scope (modern)
{
let secret = "hidden";
}
// IIFE module pattern (old)
const MyModule = (function() {
return { /* public API */ };
})();
// ES module (modern)
// myModule.js
export function publicMethod() { }
// Not exported, so private:
function privateHelper() { }

However, IIFE still has valid use cases:

// 1. Top-level await alternative (when not in a module)
(async () => {
const config = await loadConfig();
startApp(config);
})();

// 2. Isolating third-party scripts
(function() {
// All variables are private to this script
const API_KEY = "abc123";
// ... initialization code
})();

// 3. Creating a one-time setup that returns a value
const config = (function() {
const env = process.env.NODE_ENV || "development";
const baseUrl = env === "production"
? "https://api.production.com"
: "http://localhost:3000";

return Object.freeze({
env,
baseUrl,
debug: env !== "production",
});
})();
// config is computed once and frozen
info

You will encounter IIFE frequently in older JavaScript code, libraries, and bundled output. Understanding the pattern is essential for reading real-world code. In new code, prefer block scoping with let/const and ES modules for scope isolation.

Function Expressions in Practice: Patterns and Strategies

The Strategy Pattern

Function expressions stored in an object let you select behavior at runtime:

const shippingCalculators = {
standard: function(weight) {
return weight * 0.5;
},
express: function(weight) {
return weight * 1.5 + 5;
},
overnight: function(weight) {
return weight * 3 + 15;
},
};

function calculateShipping(weight, method) {
const calculator = shippingCalculators[method];

if (!calculator) {
throw new Error(`Unknown shipping method: ${method}`);
}

return calculator(weight);
}

console.log(calculateShipping(10, "standard")); // 5
console.log(calculateShipping(10, "express")); // 20
console.log(calculateShipping(10, "overnight")); // 45

Adding a new shipping method requires only adding a property to the object, without changing any if/switch logic.

The Middleware Pattern

Function expressions enable middleware-style processing chains:

function createPipeline(...middlewares) {
return function(input) {
let result = input;
for (let middleware of middlewares) {
result = middleware(result);
}
return result;
};
}

const processText = createPipeline(
function(text) { return text.trim(); },
function(text) { return text.toLowerCase(); },
function(text) { return text.replace(/\s+/g, " "); },
function(text) { return text.charAt(0).toUpperCase() + text.slice(1); }
);

console.log(processText(" HELLO BEAUTIFUL WORLD "));
// "Hello beautiful world"

Lazy Initialization

Function expressions can defer expensive computation until the first time it is needed:

const getDatabase = (function() {
let db = null;

return function() {
if (db === null) {
console.log("Connecting to database...");
db = { connected: true, data: [] }; // Expensive operation
}
return db;
};
})();

// Database is NOT connected yet
console.log("App started");

// First call: connects
const db1 = getDatabase(); // "Connecting to database..."

// Second call: returns cached connection
const db2 = getDatabase(); // No log (reuses existing connection)

console.log(db1 === db2); // true (same object)

Summary

Function expressions are fundamental to JavaScript's power and flexibility:

  • Function expressions create functions as values within larger expressions. The function keyword must not be the first token in a statement for it to be an expression.
  • Named function expressions provide a reliable self-reference for recursion and clearer names in stack traces, while the name remains invisible outside the function.
  • Functions are first-class values in JavaScript. They can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions.
  • Callbacks are function expressions passed to other functions. They are the foundation of array methods, event handlers, timers, and asynchronous patterns.
  • Anonymous functions are function expressions without a name. Modern engines infer names from assignment context. Use named expressions for complex or recursive functions.
  • Function declarations are fully hoisted (available everywhere in their scope). Function expressions follow the hoisting rules of their variable keyword (var → undefined, let/const → TDZ error).
  • Use function expressions when you need conditional creation, callbacks, or want to prevent usage before definition. Use declarations for standalone, reusable utility functions.
  • IIFE creates and immediately executes a function expression, providing a private scope. It was essential before let/const and modules. In modern code, prefer block scope and ES modules, but understand IIFE for reading legacy code.

With function expressions thoroughly understood, you are ready to learn arrow functions, the concise modern syntax that simplifies function expressions and introduces different behavior for the this keyword.