JavaScript Hoisting: Variables, Functions, and More

Abstract image used to represent JavaScript Hoisting: Variables, Functions, and More
Image by Ries Bosch.

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 blockscoped 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 hoistingrelated 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: var starts as undefined; let and const remain 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:

    Use let and const, 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.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.