How to Use Functions in JavaScript: Declarations, Expressions, Callbacks, and IIFE
Functions are the building blocks of any JavaScript program. They let you write a piece of logic once and reuse it anywhere, with different inputs and different results each time. Without functions, you would copy and paste the same code throughout your program, making it impossible to maintain.
But functions in JavaScript are more than just reusable code blocks. They are values. You can store a function in a variable, pass it as an argument to another function, and return it from a function. This "first-class" nature makes JavaScript incredibly flexible and is the foundation of patterns like callbacks, event handlers, and functional programming.
This guide covers how to create functions using declarations and expressions, why the difference matters, how callbacks work, what anonymous functions are, how hoisting affects each function type differently, and the classic IIFE pattern that shaped JavaScript for over a decade.
Function Declarations
A function declaration is the most straightforward way to create a function. It uses the function keyword followed by a name, parameters, and a body.
Basic Syntax
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet("Alice"); // "Hello, Alice!"
greet("Bob"); // "Hello, Bob!"
Parameters and Arguments
Parameters are the variable names listed in the function definition. Arguments are the actual values passed when calling the function:
function add(a, b) { // a and b are parameters
return a + b;
}
let result = add(5, 3); // 5 and 3 are arguments
console.log(result); // 8
JavaScript does not enforce the number of arguments. Missing arguments become undefined, and extra arguments are ignored:
function showArgs(a, b, c) {
console.log(a, b, c);
}
showArgs(1, 2, 3); // 1 2 3
showArgs(1, 2); // 1 2 undefined (missing c)
showArgs(1, 2, 3, 4); // 1 2 3 (extra 4 is ignored)
Default Parameters
ES6 introduced default parameter values that are used when an argument is undefined or not provided:
function greet(name = "Guest", greeting = "Hello") {
console.log(`${greeting}, ${name}!`);
}
greet("Alice", "Hi"); // "Hi, Alice!"
greet("Bob"); // "Hello, Bob!" (greeting uses default)
greet(); // "Hello, Guest!" (both use defaults)
greet(undefined, "Hey"); // "Hey, Guest!" (undefined triggers default)
Default values can be expressions, including function calls:
function createId(prefix = "user", id = Date.now()) {
return `${prefix}_${id}`;
}
console.log(createId()); // "user_1705123456789"
console.log(createId("admin")); // "admin_1705123456790"
The return Statement
The return statement sends a value back to the caller and immediately exits the function:
function multiply(a, b) {
return a * b;
console.log("This never runs"); // Unreachable code after return
}
let result = multiply(4, 5);
console.log(result); // 20
A function without a return statement (or with a bare return) returns undefined:
function doSomething() {
let x = 1 + 2;
// no return statement
}
console.log(doSomething()); // undefined
function earlyExit(value) {
if (!value) {
return; // Returns undefined
}
return value * 2;
}
console.log(earlyExit(null)); // undefined
console.log(earlyExit(5)); // 10
Functions Should Do One Thing
A well-written function has a single, clear purpose. Its name should describe what it does:
// BAD: does too many things
function processUser(user) {
// validates, formats, saves, and sends email
}
// GOOD: each function has one job
function validateUser(user) { }
function formatUserData(user) { }
function saveToDatabase(user) { }
function sendWelcomeEmail(user) { }
Function Declaration vs. Function Expression
JavaScript provides two main ways to create functions: declarations and expressions. They look similar but behave differently in important ways.
Function Declaration
A function declaration starts with the function keyword as a statement:
function square(x) {
return x * x;
}
console.log(square(5)); // 25
Function Expression
A function expression creates a function as part of an expression, typically by assigning it to a variable:
const square = function(x) {
return x * x;
};
console.log(square(5)); // 25
Notice the semicolon after the closing brace. Because this is a variable assignment statement (const square = ...;), it ends with a semicolon, just like any other assignment.
Named Function Expressions
A function expression can optionally have a name. This name is only visible inside the function itself:
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // 'fact' can be used for recursion inside
};
console.log(factorial(5)); // 120
console.log(typeof fact); // "undefined" ()'fact' is NOT visible outside)
The name in a named function expression is useful for:
- Recursion (the function can call itself reliably)
- Debugging (the name appears in stack traces instead of "anonymous")
Side-by-Side Comparison
// Function Declaration
function add(a, b) {
return a + b;
}
// Function Expression
const add = function(a, b) {
return a + b;
};
// Named Function Expression
const add = function addNumbers(a, b) {
return a + b;
};
All three create a function that adds two numbers. The differences are in hoisting, naming, and how they can be used in your code.
Functions as Values (First-Class Functions)
In JavaScript, functions are first-class citizens. This means functions are values, just like numbers, strings, and objects. You can do anything with a function that you can do with any other value.
Storing Functions in Variables
const greet = function(name) {
return `Hello, ${name}!`;
};
console.log(greet("Alice")); // "Hello, Alice!"
console.log(typeof greet); // "function"
Copying Functions
function sayHello() {
console.log("Hello!");
}
const greeting = sayHello; // Copy the reference (no parentheses!)
sayHello(); // "Hello!"
greeting(); // "Hello!" (same function, different name)
Notice that we wrote sayHello without parentheses, not sayHello(). Adding parentheses calls the function and assigns its return value. Without parentheses, we are referencing the function itself as a value.
const result = sayHello(); // Calls the function, result = undefined
const func = sayHello; // References the function, func is the function itself
Storing Functions in Data Structures
// Functions in an array
const operations = [
function(a, b) { return a + b; },
function(a, b) { return a - b; },
function(a, b) { return a * b; },
function(a, b) { return a / b; },
];
console.log(operations[0](10, 5)); // 15 (addition)
console.log(operations[2](10, 5)); // 50 (multiplication)
// Functions in an object (methods)
const calculator = {
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; },
multiply: function(a, b) { return a * b; },
};
console.log(calculator.add(10, 5)); // 15
console.log(calculator.multiply(10, 5)); // 50
Returning Functions from Functions
A function can create and return another function:
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(100)); // 200
createMultiplier is a function factory. Each call produces a new function with a different factor baked in. This pattern is an example of closures, which are covered in depth later in the course.
Why This Matters
The first-class nature of functions is what makes JavaScript uniquely powerful. It enables:
- Callback patterns (passing functions to other functions)
- Event handlers (
button.addEventListener("click", handleClick)) - Array methods (
array.map(transformFunction)) - Functional programming (composing small functions into complex behavior)
- Closures and factory functions (creating specialized functions)
Callback Functions: Passing Functions as Arguments
A callback is a function passed as an argument to another function, which then calls it at some point. Callbacks are one of the most important patterns in JavaScript.
Basic Callback Example
function processUserInput(callback) {
let name = prompt("Enter your name:");
callback(name);
}
function greet(name) {
console.log(`Hello, ${name}!`);
}
function farewell(name) {
console.log(`Goodbye, ${name}!`);
}
processUserInput(greet); // Asks for name, then prints "Hello, [name]!"
processUserInput(farewell); // Asks for name, then prints "Goodbye, [name]!"
The processUserInput function does not know or care what happens with the name. It delegates that responsibility to whatever callback is provided. This is the core idea of callbacks: separation of concerns.
Callbacks with Array Methods
JavaScript's built-in array methods heavily use callbacks:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// .filter() calls the callback for each element
// Keeps elements where the callback returns true
let evenNumbers = numbers.filter(function(num) {
return num % 2 === 0;
});
console.log(evenNumbers); // [2, 4, 6, 8, 10]
// .map() calls the callback for each element
// Creates a new array with the callback's return values
let doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// .forEach() calls the callback for each element (no return value)
numbers.forEach(function(num, index) {
console.log(`Index ${index}: ${num}`);
});
// Index 0: 1
// Index 1: 2
// Index 2: 3
// Index 3: 4
// Index 4: 5
// Index 5: 6
// Index 6: 7
// Index 7: 8
// Index 8: 9
// Index 9: 10
Custom Functions That Accept Callbacks
function repeat(action, times) {
for (let i = 0; i < times; i++) {
action(i);
}
}
repeat(function(i) {
console.log(`Iteration ${i}`);
}, 3);
// Iteration 0
// Iteration 1
// Iteration 2
repeat(console.log, 3);
// 0
// 1
// 2
// A more practical example: transform and log
function transformAndDisplay(data, transformer, formatter) {
let result = transformer(data);
let output = formatter(result);
console.log(output);
}
transformAndDisplay(
"hello world",
function(str) { return str.toUpperCase(); },
function(str) { return `*** ${str} ***`; }
);
// *** HELLO WORLD ***
Event Handlers Are Callbacks
When you add an event listener, you are passing a callback:
// The function passed to addEventListener is a callback
// The browser calls it when the event occurs
document.getElementById("myButton").addEventListener("click", function() {
console.log("Button was clicked!");
});
// You can also pass a named function
function handleClick() {
console.log("Button was clicked!");
}
document.getElementById("myButton").addEventListener("click", handleClick);
Timing Functions Use Callbacks
// setTimeout calls the callback after a delay
setTimeout(function() {
console.log("This runs after 2 seconds");
}, 2000);
// setInterval calls the callback repeatedly
setInterval(function() {
console.log("This runs every 3 seconds");
}, 3000);
Anonymous Functions
An anonymous function is a function without a name. You have already seen many of them in the callback examples above.
What Makes a Function Anonymous
// Named function
function greet(name) {
return `Hello, ${name}`;
}
// Anonymous function (no name after 'function')
const greet = function(name) {
return `Hello, ${name}`;
};
The second example creates a function with no name and assigns it to the variable greet. The variable has a name, but the function itself is anonymous.
Where Anonymous Functions Are Used
Anonymous functions are most commonly used as:
Inline callbacks (the most common use):
// Instead of defining a named function and then passing it...
function isEven(num) {
return num % 2 === 0;
}
let evens = numbers.filter(isEven);
// ...you write the function inline
let evens = numbers.filter(function(num) {
return num % 2 === 0;
});
Immediately executed code:
(function() {
console.log("I run immediately and have no name");
})();
Event handlers:
button.addEventListener("click", function() {
console.log("Clicked!");
});
The Name Inference Feature
Modern JavaScript engines automatically infer a name for anonymous function expressions based on the variable or property they are assigned to:
const greet = function(name) {
return `Hello, ${name}`;
};
console.log(greet.name); // "greet" (inferred from variable name)
const obj = {
sayHi: function() {
return "Hi!";
}
};
console.log(obj.sayHi.name); // "sayHi" (inferred from property name)
This inferred name appears in stack traces and debugging, making anonymous functions much easier to debug than they were in older JavaScript.
Named vs. Anonymous: When to Choose Which
// Use anonymous functions for short, inline callbacks
let sorted = items.sort(function(a, b) { return a - b; });
// Use named functions when:
// 1. The function is complex (more than a few lines)
// 2. You need to reuse it in multiple places
// 3. You want a clear name in stack traces for debugging
// 4. The function calls itself (recursion)
function compareByPrice(a, b) {
if (a.price === b.price) return 0;
return a.price > b.price ? 1 : -1;
}
let byPrice = items.sort(compareByPrice);
In practice, modern JavaScript uses arrow functions (covered in the next chapter) instead of anonymous function expressions for most callbacks. Arrow functions are more concise and handle this differently:
// Anonymous function expression
let evens = numbers.filter(function(n) { return n % 2 === 0; });
// Arrow function (modern equivalent)
let evens = numbers.filter(n => n % 2 === 0);
Hoisting Differences: Declarations vs. Expressions
Function declarations and function expressions are hoisted differently, and this difference has real consequences for how you structure your code.
Function Declarations Are Fully Hoisted
A function declaration is available throughout its entire scope, even before the line where it appears in the code:
// This works! The function can be called before it's declared
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
JavaScript processes all function declarations during the compilation phase, before any code executes. The entire function (name + body) is hoisted to the top of its scope.
// JavaScript sees the above code as:
function sayHello() { // Hoisted: entire function moved to top
console.log("Hello!");
}
sayHello(); // "Hello!"
This works across the entire scope, even in complex code:
console.log(add(2, 3)); // 5 (works before declaration)
if (true) {
console.log("Between");
}
function add(a, b) {
return a + b;
}
Function Expressions Are NOT Fully Hoisted
A function expression assigned to a variable follows the hoisting rules of that variable:
With var: The variable is hoisted but initialized as undefined. Calling it before the assignment throws a TypeError:
console.log(typeof greet); // "undefined" (variable exists but is undefined)
greet("Alice"); // TypeError: greet is not a function
var greet = function(name) {
console.log(`Hello, ${name}!`);
};
greet("Alice"); // "Hello, Alice!" (works after assignment)
The engine sees this as:
var greet; // Declaration hoisted, value is undefined
console.log(typeof greet); // "undefined"
greet("Alice"); // TypeError: undefined is not a function
greet = function(name) { // Assignment stays here
console.log(`Hello, ${name}!`);
};
With let or const: The variable is in the Temporal Dead Zone. Accessing it before the declaration throws a ReferenceError:
greet("Alice"); // ReferenceError: Cannot access 'greet' before initialization
const greet = function(name) {
console.log(`Hello, ${name}!`);
};
Practical Implications
// This works because function declarations are hoisted
function main() {
let result = calculate(10, 5);
display(result);
}
function calculate(a, b) {
return a + b;
}
function display(value) {
console.log(`Result: ${value}`);
}
main(); // "Result: 15"
You can call calculate and display inside main even though they are declared after main. This lets you put the high-level logic first and the implementation details later, which some developers find more readable.
// This does NOT work with function expressions
const main = function() {
let result = calculate(10, 5); // ReferenceError!
display(result);
};
const calculate = function(a, b) {
return a + b;
};
const display = function(value) {
console.log(`Result: ${value}`);
};
main();
With function expressions, you must define functions before you use them.
Comparison Table
| Aspect | Function Declaration | Function Expression |
|---|---|---|
| Syntax | function name() {} | const name = function() {}; |
| Hoisted? | Yes, fully (name + body) | Variable only (follows var/let/const rules) |
| Usable before declaration? | Yes | No |
| Can be conditional? | Technically yes, but behavior varies | Yes, naturally |
| Has a name? | Always | Optional (inferred from variable) |
| Requires semicolon? | No | Yes (it is an assignment) |
Conditional Function Creation
Function expressions are the correct way to create functions conditionally:
// CORRECT: function expressions work in conditions
let greet;
let language = prompt("Language")
if (language === "en") {
greet = function(name) { return `Hello, ${name}!`; };
} else if (language === "es") {
greet = function(name) { return `¡Hola, ${name}!`; };
} else {
greet = function(name) { return `Hi, ${name}!`; };
}
console.log(greet("Alice"));
Function declarations inside if blocks have inconsistent behavior across browsers and strict/non-strict modes. Avoid this pattern:
// AVOID: function declarations inside blocks
// Behavior varies between browsers and strict/non-strict mode
if (condition) {
function greet() { return "Hello!"; } // Don't do this
}
Immediately Invoked Function Expressions (IIFE)
An IIFE (pronounced "iffy") is a function that is created and executed immediately, in a single expression. It was one of the most important patterns in JavaScript before let, const, and modules were introduced.
Basic Syntax
(function() {
console.log("I run immediately!");
})();
// Output: "I run immediately!"
Let us break down the syntax:
(function() { // 1. Wrap the function in parentheses (makes it an expression)
// function body
})(); // 2. Add () to call it immediately
The outer parentheses are required. Without them, JavaScript sees function as the start of a function declaration, which cannot be immediately invoked:
// SyntaxError: Function statements require a function name
function() {
console.log("This doesn't work");
}();
// Works: parentheses make it an expression
(function() {
console.log("This works!");
})();
Alternative Syntax Forms
There are several valid ways to write an IIFE:
// Parentheses outside (most common)
(function() {
console.log("Style 1");
})();
// Parentheses inside (also common)
(function() {
console.log("Style 2");
}());
// Using unary operators (less common)
!function() {
console.log("Style 3");
}();
+function() {
console.log("Style 4");
}();
void function() {
console.log("Style 5");
}();
IIFE with Parameters
(function(name, greeting) {
console.log(`${greeting}, ${name}!`);
})("Alice", "Hello");
// Output: "Hello, Alice!"
IIFE with Return Values
const result = (function() {
let a = 10;
let b = 20;
return a + b;
})();
console.log(result); // 30
Why IIFE Exists: Creating Private Scope
Before let and const (which have block scope), var was the only way to declare variables, and var has function scope. The only way to create a private scope was to wrap code in a function. IIFE provided that function scope without leaving a named function behind.
The problem IIFE solved:
// Without IIFE: variables leak into global scope
var counter = 0;
var name = "MyApp";
// These pollute the global namespace and can conflict with other scripts
The IIFE solution:
// With IIFE: variables are contained
(function() {
var counter = 0;
var name = "MyApp";
// These variables are private to the IIFE
// They don't pollute the global scope
})();
console.log(typeof counter); // "undefined" (not accessible)
console.log(typeof name); // "undefined" (not accessible)
The Module Pattern (Classic JavaScript Pattern)
Before ES6 modules, IIFE was the standard way to create modules with public and private members:
const counterModule = (function() {
// Private variable (not accessible outside)
let count = 0;
// Return an object with public methods
return {
increment() {
count++;
},
decrement() {
count--;
},
getCount() {
return count;
}
};
})();
counterModule.increment();
counterModule.increment();
counterModule.increment();
console.log(counterModule.getCount()); // 3
console.log(counterModule.count); // undefined (private!)
IIFE in Modern JavaScript
With let, const, and ES modules, the original need for IIFE has largely disappeared:
// IIFE for scope (old way)
(function() {
var temp = "private";
console.log(temp);
})();
// Block scope with let/const (modern way)
{
let temp = "private";
console.log(temp);
}
// ES modules (modern way for separate files)
// Each module file has its own scope automatically
However, IIFE is still relevant in several situations:
When you need to await at the top level without module support:
(async function() {
let response = await fetch("https://api.example.com/data");
let data = await response.json();
console.log(data);
})();
When adding a script to an existing page and you want to avoid variable conflicts:
// Third-party script that shouldn't leak variables
(function() {
const config = { apiKey: "abc123" };
// All variables are scoped to this IIFE
})();
When you encounter IIFE in existing codebases (there is a lot of pre-ES6 code still in production):
// jQuery plugins traditionally use IIFE
(function($) {
$.fn.myPlugin = function() {
// plugin code
};
})(jQuery);
You do not need to use IIFE in new modern JavaScript code. Block scoping (let/const with { }) and ES modules handle scope isolation. But understanding IIFE is important because you will encounter it in older codebases, libraries, and tutorials. It is also a fundamental demonstration of how first-class functions and closures work together.
Putting It All Together: A Practical Example
Here is an example that demonstrates declarations, expressions, callbacks, and the first-class nature of functions working together:
// Function declaration: defines a data processing pipeline
function processSalesData(data, filters, transformer, reporter) {
let filtered = data;
// Apply each filter (callbacks)
for (let filter of filters) {
filtered = filtered.filter(filter);
}
// Transform the data (callback)
let transformed = filtered.map(transformer);
// Report the results (callback)
reporter(transformed);
}
// Function expressions: specific implementations
const isHighValue = function(sale) {
return sale.amount > 100;
};
const isRecent = function(sale) {
return sale.daysAgo < 30;
};
// Named function (better for stack traces)
function formatSale(sale) {
return `${sale.customer}: $${sale.amount}`;
}
// Sample data
const sales = [
{ customer: "Alice", amount: 250, daysAgo: 5 },
{ customer: "Bob", amount: 50, daysAgo: 10 },
{ customer: "Charlie", amount: 175, daysAgo: 45 },
{ customer: "Diana", amount: 300, daysAgo: 2 },
];
// Using the pipeline with different combinations
processSalesData(
sales,
[isHighValue, isRecent], // Array of filter callbacks
formatSale, // Transformer callback
function(results) { // Anonymous reporter callback
console.log("High-value recent sales:");
results.forEach(r => console.log(` ${r}`));
}
);
// High-value recent sales:
// Alice: $250
// Diana: $300
This example shows:
- Function declarations for the main pipeline function
- Function expressions stored in variables for reusable filters
- Named functions for important utility logic
- Anonymous functions for one-off inline callbacks
- Callbacks passed as arguments to customize behavior
- Functions as values stored in arrays and passed around freely
Summary
Functions are the most important concept in JavaScript. Here is what you need to remember:
- Function declarations use
function name() {}syntax, are fully hoisted, and can be called before they appear in the code. - Function expressions assign a function to a variable (
const fn = function() {}). They follow the hoisting rules of their variable keyword and cannot be called before the assignment line. - JavaScript functions are first-class values. They can be stored in variables, passed as arguments, returned from other functions, and placed in arrays and objects.
- Callbacks are functions passed as arguments to other functions. They are the foundation of event handling, array methods, and asynchronous programming in JavaScript.
- Anonymous functions have no name and are commonly used for short inline callbacks. Modern engines infer names from variable assignments for debugging.
- Function declarations are fully hoisted (usable before the declaration line). Function expressions are not (they follow
var/let/consthoisting rules). - IIFE creates and executes a function immediately, providing a private scope. It was essential before
let/constand modules, and is still found in older codebases. - Use function declarations for top-level, reusable functions. Use function expressions when you need conditional function creation, want to prevent hoisting, or need to pass a function as a value.
With a solid understanding of function declarations and expressions, you are ready to learn arrow functions, the modern, concise syntax for creating functions that also handles the this keyword differently.