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
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.
| Declaration | Before the declaration line |
|---|---|
function name() {} | Callable, body included |
var name | Exists, holds undefined |
let name and const name | Exists, 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.
sayHello(); // works
function sayHello() {
console.log("hello");
}
console.log(counter); // undefined, not an error
var counter = 3;
console.log(counter); // 3The 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.
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
construles. - Forget
var. Its silent hoisting toundefinedis the only real source of trouble here.
Frequently asked questions
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.
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.
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.