Function Declarations vs Function Expressions in JavaScript: Hoisting and Scope Explained

Function declarations are fully hoisted with their definition, allowing invocation before they appear in source code, while function expressions are not hoisted as callable values—only the variable declaration is raised—resulting in a TypeError if called before the assignment line executes.

The h5bp/Front-end-Developer-Interview-Questions repository explicitly tests this distinction at line 20 of src/questions/javascript-questions.md, where candidates must compare function foo() {} against var foo = function() {}. Mastering the differences between function declarations and function expressions is critical for understanding JavaScript execution contexts, managing scope pollution, and implementing advanced patterns like IIFEs and callbacks.

Hoisting and Execution Order

The JavaScript engine processes function declarations and expressions differently during the compilation phase. Function declarations are hoisted completely—the engine moves the entire function body to the top of the containing scope, making the identifier available throughout the entire block.

Function expressions follow variable hoisting rules. When declared with var, the identifier is hoisted and initialized as undefined, but the function assignment remains at its original position. When using let or const, the identifier exists in the temporal dead zone until execution reaches the declaration, preventing any access before initialization.

Function Declaration Hoisting

Because the entire function definition is hoisted, you can safely call declared functions before their textual position in the code.

console.log(declared()); // ✅ "I am available"

function declared() {
  return 'I am available';
}

Function Expression Hoisting Behavior

Attempting to invoke a function expression before its assignment line results in different errors depending on the declaration keyword.

// With var: TypeError because foo is undefined
console.log(foo()); // ❌ TypeError: foo is not a function
var foo = function() { return 'assigned'; };

// With let: ReferenceError due to temporal dead zone
console.log(bar()); // ❌ ReferenceError: Cannot access 'bar' before initialization
let bar = function() { return 'initialized'; };

Naming Conventions and Scope Isolation

Function declarations always expose their name to the containing scope, creating a bound identifier that persists for the entire lexical scope. Function expressions may be anonymous or named, with named function expressions creating an internal identifier visible only within the function body—useful for recursion without leaking names into outer scopes.

var factorial = function fac(n) {
  return n <= 1 ? 1 : n * fac(n - 1);
};
// fac is undefined here; only factorial is available in outer scope

Both declarations and expressions determine this binding at invocation time based on the call site, not the definition style. However, expressions often appear in contexts where closures capture lexical scope, particularly when passed as arguments or returned from factory functions.

Usage Patterns and Value Semantics

Function expressions behave as first-class values—they can be passed to higher-order functions, returned as closures, stored in data structures, or immediately invoked to create private scopes. Function declarations serve better as standalone utilities where hoisting enables flexible code organization across large files.

Immediately Invoked Function Expressions (IIFE)

Function expressions enable the IIFE pattern, creating execution contexts isolated from the global scope.

(function() {
  const privateData = 'encapsulated';
  console.log(privateData); // ✅ 'encapsulated'
})();
// privateData is not accessible here

Callbacks and Higher-Order Functions

Because expressions are values, they integrate seamlessly into asynchronous patterns and functional programming constructs.

setTimeout(function() {
  console.log('Executed after delay');
}, 1000);

const operations = [
  function(a) { return a * 2; },
  function(a) { return a + 3; }
];

Summary

  • Function declarations are fully hoisted to the top of their scope, including the function body, enabling invocation before the declaration appears in source code.
  • Function expressions assign functions to variables; only the variable declaration is hoisted when using var, while let/const bindings remain inaccessible until execution reaches the assignment.
  • The h5bp interview questions at src/questions/javascript-questions.md line 20 specifically evaluate understanding of these hoisting mechanics to assess candidate knowledge of JavaScript execution models.
  • Named function expressions provide local identifiers for recursion without polluting the enclosing scope, unlike function declarations which always bind their name to the surrounding lexical environment.
  • Function expressions support patterns impossible with declarations, including IIFEs for encapsulation and passing functions as arguments where values are required.

Frequently Asked Questions

What happens if you call a function expression before its assignment?

Calling a function expression before the assignment line executes throws a TypeError: foo is not a function when declared with var, because the identifier exists but holds undefined. With let or const, JavaScript throws a ReferenceError because the variable remains in the temporal dead zone until initialization.

Can function expressions have names, and where is that name available?

Yes, function expressions may include names (var x = function named() {}), but unlike function declarations, that name is only accessible within the function's own scope. This allows reliable self-reference for recursion without exposing identifiers to the global or enclosing scopes.

Which offers better performance: function declarations or expressions?

According to the h5bp source analysis, function declarations may parse slightly faster in some JavaScript engines because the engine can identify them during the initial compilation pass. However, the difference is typically negligible; choose based on hoisting needs and scope requirements rather than micro-optimizations.

When should I use a function expression instead of a declaration?

Use function expressions when you need to control the exact moment of function creation, pass functions as values to other functions, create closures with IIFEs, or avoid polluting scope with unnecessary names. They signal intent that the function is a value consumed at a specific execution point, while declarations indicate reusable utilities meant for the entire scope.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →