JavaScript has a few quirks that often surprise developers, especially those just getting started. One of the most important concepts to understand is hoisting. If you’ve ever wondered why you can call some functions before they’re defined, or why variables sometimes behave unexpectedly, hoisting is the answer.
In this post, we’ll break down what hoisting is, how it works, and common pitfalls you should avoid.
What Is Hoisting?
Hoisting is JavaScript’s default behavior of moving variable and function declarations to the top of their scope (either the global scope or the current function scope) during the compilation phase.
This doesn’t mean the code itself is physically rearranged. Instead, JavaScript’s engine sets aside memory for variable and function declarations before executing the code.
Function Hoisting
Functions declared using the function
keyword are fully hoisted. That means you can call them before their definition appears in the code. Personally, I prefer this style because it allows you to write the meat of your JavaScript module — the main bits, up top and leave the function declarations towards the bottom of the page. This way another developer looking at a module can figure out the gist of what your code is doing and only dig into function definitions as necessary.
sayHello(); // ✅ Works! function sayHello() { console.log("Hello, world!"); }
Why does this work? Because the entire function declaration (name + body) is hoisted to the top of the scope.
Variable Hoisting with var
Variables declared with var
are also hoisted, but there’s a catch: only the declaration is hoisted, not the initialization.
console.log(a); // undefined var a = 10; console.log(a); // 10
Here’s what JavaScript actually “sees”:
var a; console.log(a); // undefined a = 10; console.log(a); // 10
This can lead to confusing bugs if you assume a doesn’t exist at all before the declaration.
Hoisting with let
and const
let
and const
were introduced in ES6 to fix some of the weird behavior of var.
• They are also hoisted, but they go into a “temporal dead zone” (TDZ) from the start of the block until their declaration is encountered.
• This means you cannot access them before they are declared, otherwise you’ll get a runtime error.
console.log(b); // ❌ ReferenceError let b = 20; console.log(c); // ❌ ReferenceError const c = 30;
Hoisting with Function Expressions and Arrow Functions
If you assign a function to a variable (using var
, let
, or const
), only the variable is hoisted, not the function definition.
sayHi(); // ❌ TypeError: sayHi is not a function var sayHi = function() { console.log("Hi!"); };
With var
, the variable is hoisted but initialized to undefined, so calling it like a function fails.
With let
or const
, you’ll hit the temporal dead zone.
Common Pitfalls of Hoisting
Using variables before declaration
console.log(count); // undefined var count = 5;
Assuming function expressions behave like function declarations
greet(); // ❌ Error const greet = () => console.log("Hello");
Forgetting about block scope with let
and const
{ console.log(x); // ❌ ReferenceError let x = 100; }
Best Practices to Avoid Hoisting Confusion
- Always declare variables at the top of their scope.
- Prefer let and const over var to avoid unexpected undefined values.
- If you like to put functions towards the end of the file (like I do), make sure that they are defined using the function keyword and not using the arrow syntax.
- Be mindful of the temporal dead zone with
let
andconst
.
Conclusion
Hoisting is one of those JavaScript features that can feel odd at first, but once you understand it, you’ll write cleaner, less error-prone code.
To recap:
• Function declarations are fully hoisted.
• var declarations are hoisted but initialized to undefined.
• let and const are hoisted but live in the temporal dead zone.
• Function expressions and arrow functions behave like variables, not like function declarations.
By keeping these rules in mind, you’ll avoid many headaches and write code that behaves exactly the way you expect.