Hoisting in JavaScript: why a variable exists before its declaration

Hoisting makes declarations known to the engine before the first line runs. Functions become callable early, variables do not become readable.
3 min read
Believemy logo

Calling a function ten lines before writing it works. Reading a variable before its declaration returns undefined in one case and throws an error in another. All three behaviors come from the same mechanism.

It is usually summed up with a misleading picture, that of declarations floating to the top of the file. Nothing actually moves: the engine simply learns the declared names before running a single line.


Definition

One read line, six declarations, three outcomes
Declaration
line 2
temporal dead zone
1// ...
2console.log(total);
3let total = 3;
past the declaration, normal read
4// ...
5// ...
Console
✗ReferenceError: Cannot access 'total' before initialization
At line 2, what the six declarations give
value1
undefined1
ReferenceError4

The name exists, but it stays out of reach until its declaration line. That interval has a name, the temporal dead zone. Rather than hand back a misleading value, the language refuses the read and the error points at the offending line.

The twenty-four states of this demo, six declarations by four read lines, were each run as they stand under Node: the outputs and error messages shown here are its own. The // ... lines do nothing, they only mark the spots where the read can sit.

Change the keyword and where the read happens. The same code gives a value or an error, and the keyword is what decides.

Hoisting describes the fact that the declarations of a block are registered by the engine before that block runs. The name therefore exists from the very first line. What changes from one keyword to the next is the value it holds at that moment.

DeclarationBefore the declaration line
function name() {}Callable, body included
var nameExists, holds undefined
let name and const nameExists, but any read throws
class Name {}Exists, but any read throws
const name = () => {}Follows the const rule, not the function one

That two-column table explains almost every surprise on the topic.

JAVASCRIPT
sayHello();            // works
function sayHello() {
  console.log("hello");
}

console.log(counter);  // undefined, not an error
var counter = 3;
console.log(counter);  // 3


The temporal dead zone

Between the start of the block and the declaration line, a let or const variable exists without being readable. That interval has a name, the temporal dead zone, and any read falling inside it fails.

JAVASCRIPT
try {
  console.log(total);
  let total = 3;
} catch (error) {
  console.log(error.constructor.name); // ReferenceError
}

This is a deliberate design choice: rather than hand back a misleading value, the language refuses the read. The error points straight at the offending line, where var used to let an undefined through that only surfaced much later.


What to do with it day to day

  • Declare before using. The rule makes hoisting invisible, which is the best possible state.
  • Rely on function hoisting. Keeping helper functions at the bottom of a file remains perfectly sound.
  • Do not rely on it for a Arrow function. Stored in a constant, it follows the const rules.
  • Forget var. Its silent hoisting to undefined is the only real source of trouble here.


Frequently asked questions

Question

Is the code really moved around inside the file?

No, and that is the main misunderstanding. Nothing is rewritten or relocated. The engine walks the block once to register the declared names, then runs the lines in the order they were written. The image of things rising to the top is a teaching metaphor, not a description of the machinery.


Question

Why does a function expression not get hoisted?

Because what gets declared is the variable, not the function. With const process = function () {}, only the name process is registered, and its value is assigned when that line runs. Calling process() earlier therefore fails like any other premature read.


Question

Do function parameters follow the same rules?

They are registered before the function body, which is why a Default parameter can build on a parameter declared before it. A let declaration in the body, however, cannot reuse a parameter name: the engine reports the conflict as soon as it parses the file.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.