JavaScript Hoisting: Variables, Functions, and More

In Brief
Hoisting is best understood as bindings being created whilst JavaScript sets up an execution context, not as source code physically moving upwards. Function declarations are initialised with their function, var starts as undefined, and let, const and classes remain inaccessible in the temporal dead zone until their declarations run. Write code in declaration order even when earlier access happens to work.
JavaScript prepares bindings before execution reaches their declarations, but "hoisting" can make that sound more magical than it is. Function declarations, var, let and const have different initialisation rules, which determine whether earlier access works or throws an error.
What is Hoisting?
Hoisting describes the effect of preparing bindings as an execution context or lexical scope is set up. It does not mean moving the source code. A function declaration can make its function available before execution reaches the declaration. A var binding starts as undefined, whereas a let or const binding remains uninitialised until its declaration is evaluated.
For example:
console.log(myVar); // undefined
var myVar = 'Hello';Here, the myVar binding already exists and is initialised to undefined when console.log() runs. The later assignment gives it the string value. No declaration has moved in the source.
Hoisting in Variables
var and Hoisting
A var binding is created in its variable scope and initialised to undefined before the surrounding statements run. An initialiser such as = "Hi!" is still evaluated when execution reaches that statement.
Take this code for example:
console.log(greeting); // undefined
var greeting = 'Hi!';We can illustrate that timing with this equivalent arrangement. This is an explanatory rewrite, rather than source code produced by the engine:
var greeting;
console.log(greeting); // undefined
greeting = 'Hi!';let and const
Variables declared with let and const are also hoisted, but they remain in a "temporal dead zone" (TDZ) until their declaration is evaluated.
console.log(message); // ReferenceError: Cannot access 'message' before initialisation
let message = 'Hello!';The temporal dead zone ensures that you cannot access the variable before it is declared, which eliminates one of the common pitfalls associated with var, and means that we get a ReferenceError instead of undefined.
Hoisting in Functions
Function Declarations
Function declarations are fully hoisted, meaning you can call the function before it is defined in your code.
For example:
sayHello();
function sayHello() {
console.log('Hello, world!');
}The function binding is initialised with the function when its scope is prepared, which is why this earlier call succeeds.
Function Expressions
A function expression creates its function value when the expression is evaluated. Before that assignment, a variable declared with var holds undefined, so trying to call it throws a TypeError. A let or const binding would instead be in its temporal dead zone and throw a ReferenceError:
sayHello(); // TypeError: sayHello is not a function
var sayHello = function () {
console.log('Hello, world!');
};Again, this equivalent arrangement illustrates the timing of the var binding and the later assignment:
var sayHello;
sayHello(); // TypeError
sayHello = function () {
console.log('Hello, world!');
};Best Practices for Managing Hoisting
1. Use let and const Instead of var
Avoid var in modern JavaScript (there really is very little reason you would ever use var) to prevent unexpected behaviour due to hoisting. Stick to let and const for block‑scoped variables.
const name = 'Sophie';
let age = 30;2. Declare Variables and Functions at the Top of Their Scope
Explicitly declaring variables and functions at the beginning of their scope reduces the potential for hoisting‑related confusion. It also makes it much easier to read, especially for the developer who comes to your code after you!
function processData() {
let data;
// Logic goes here
}3. Initialise Variables Immediately
Wherever possible, declare and initialise variables in a single step to avoid accessing uninitialised values.
let user = 'John';Wrapping Up
Key Takeaways
- What is hoisting? Bindings are prepared before execution reaches their declarations; source code is not moved.
- Variables:
varstarts asundefined;letandconstremain uninitialised in the temporal dead zone until their declarations are evaluated. - Functions: Function declarations can be available during scope setup. Function expressions produce their values when the expression is evaluated.
Best practices:
Useletandconst, declare variables and functions at the start of their scope, and initialise variables immediately.
"Hoisting" is useful shorthand for how declarations affect bindings before execution reaches their position in the source; the code is not physically moved. Function declarations can be called before that position. A var binding reads as undefined before its assignment, while let and const bindings remain in the temporal dead zone until their declarations are evaluated.