JavaScript Hoisting: Why Some Code Runs Before You Wrote It

Hoisting explains why a function works before its definition, why a var is undefined instead of an error, and why let throws in the temporal dead zone. Understand what JavaScript actually moves to the top — and what it doesn't.

Published August 7, 20269 min readBy ACY Partner Indonesia
JavaScript hoisting — declarations lifted to the top of their scope
300 × 250Ad Space AvailablePlace your ad here

Sooner or later you’ll write JavaScript that shouldn’t work but does — a function you call several lines before you define it, and yet it runs fine. Or the opposite: a variable that logs undefined when you were sure it would either hold a value or throw an error. Both of these come down to one mechanism with a slightly dramatic name: hoisting.

Hoisting is the behavior where JavaScript appears to move declarations to the top of their scope before any code runs. “Appears to” is the important part — nothing physically jumps around in your file. But understanding the model explains a whole class of confusing bugs, and it’s the natural companion to scope and closures, which you’ll want comfortable in your head before reading on.

What hoisting actually is

When the JavaScript engine runs your code, it doesn’t go strictly line by line from the start. It works in two phases. First comes a creation phase, where it scans the current scope and registers every declaration it finds — every var, every function, every let, const, and class. Only after that does the execution phase begin, running your statements top to bottom.

Because declarations are registered up front, a name can be known to the engine before the line where you wrote it. That’s the entire trick. The catch is that being known is not the same as having a value — and the different keywords behave very differently on that point.

console.log(message);   // undefined — known, but no value yet
var message = "Hello";
console.log(message);   // "Hello"

A useful mental model: the engine rewrites the code so the declaration lifts to the top, while the assignment stays exactly where you wrote it.

// How the engine effectively sees it:
var message;            // declaration hoisted up
console.log(message);   // undefined
message = "Hello";      // assignment stays put
console.log(message);   // "Hello"

That’s why the first log is undefined rather than a “not defined” error. The name exists; the value hasn’t arrived.

How var gets hoisted

Variables declared with var are hoisted and initialized to undefined during the creation phase. So you can reference them before their line without crashing — you just get undefined until the assignment runs.

function greet() {
  console.log(name);    // undefined (not an error)
  var name = "John Doe";
  console.log(name);    // "John Doe"
}
greet();

There’s a second var quirk worth knowing: it’s function-scoped, not block-scoped. A var declared inside an if or a for is hoisted to the top of the whole function, not just the block it sits in.

function check(score) {
  if (score > 50) {
    var result = "pass";
  }
  console.log(result);   // "pass" if score > 50, otherwise undefined — never an error
}

Even though result was written inside the if block, var lifts it to the top of check, so the final console.log can still see it. This is exactly the kind of leakage that let and const were designed to stop.

var hoisting hides bugs

Because a var reference before assignment quietly returns undefined instead of throwing, a typo or a logic slip can go unnoticed. You read undefined, assume the data simply wasn’t there, and chase the bug in the wrong place. This silent failure is one of the main reasons modern JavaScript steers you away from var entirely.

let and const: the temporal dead zone

let and const are hoisted too — this surprises people who’ve heard “only var is hoisted.” The difference is that they are not initialized to undefined. They’re registered during the creation phase, but they stay uninitialized until the engine reaches their actual line. Touch them before that point and you get a ReferenceError.

console.log(count);   // ReferenceError: Cannot access 'count' before initialization
let count = 5;

That gap — from the top of the scope until the declaration line, where the name exists but can’t be touched — is called the temporal dead zone, or TDZ. “Temporal” because it’s about time (a stretch of execution), not a physical region of the file.

{
  // TDZ for `total` starts here
  // console.log(total);  // ReferenceError if uncommented
  const total = 100;      // TDZ ends — `total` is now usable
  console.log(total);     // 100
}

This is intentional, and it’s a feature, not a flaw. Instead of the quiet undefined you’d get from var, the TDZ makes the mistake loud and immediate. You find out at the exact spot you used a variable too early, instead of debugging a confusing undefined three functions away.

Why the TDZ is on your side

The temporal dead zone turns a silent class of bugs into an obvious error message. If you see “Cannot access ‘x’ before initialization,” it’s not a JavaScript flaw — it’s the language telling you precisely where you used a variable before declaring it. Fix it by moving the declaration above its first use. This is one more reason to reach for let and const over var.

Function hoisting: declarations vs expressions

Functions are where hoisting feels almost magical — but only one kind of function gets the full treatment.

A function declaration (the function name() {} form) is hoisted completely: both the name and the body. That’s why you can call it before it appears in the file.

sayHi();   // "Hi!" — works, even though it's defined below

function sayHi() {
  console.log("Hi!");
}

The engine registered the entire function during the creation phase, so by the time execution reaches sayHi(), the function is fully ready.

A function expression — assigning a function to a variable — is a different story. Here, only the variable is hoisted, following the rules of whichever keyword you used. The function value itself isn’t available until the assignment line runs.

sayBye();   // TypeError: sayBye is not a function

var sayBye = function () {
  console.log("Bye!");
};

Because sayBye was declared with var, the name is hoisted and set to undefined. Calling undefined() throws a TypeError — note it’s not a ReferenceError, because the name does exist; it just isn’t a function yet. Swap in let or const and you’d hit the TDZ instead, getting a ReferenceError for using it too early.

Arrow functions behave exactly like function expressions, since they’re always assigned to a variable:

hello();   // ReferenceError (const is in the TDZ)

const hello = () => console.log("Hello");

Declarations vs expressions, at a glance

A function declaration (function foo() {}) is hoisted whole — name and body — so you can call it before it’s written. A function expression or arrow function (const foo = () => {}) hoists only the variable, by the rules of var/let/const — so calling it early gives you undefined/TypeError (var) or a ReferenceError (let/const). When in doubt, define before you use.

A worked example tying it together

Here’s a single snippet that exercises every rule above. Try predicting each line before reading the comments:

function demo() {
  console.log(a);        // undefined — var hoisted, no value yet
  // console.log(b);     // ReferenceError — b is in the TDZ
  declared();            // "I'm a declaration" — fully hoisted
  // expressed();        // TypeError — expressed is undefined here

  var a = 1;
  let b = 2;

  function declared() {
    console.log("I'm a declaration");
  }

  var expressed = function () {
    console.log("I'm an expression");
  };

  console.log(a, b);     // 1 2 — both assigned by now
  expressed();           // "I'm an expression" — now it's a function
}

demo();

Read top to bottom, the surprises vanish: a exists but is undefined, b is untouchable in its TDZ, declared is ready immediately, and expressed only becomes callable after its assignment runs. Every result follows directly from which keyword introduced the name.

How to avoid hoisting surprises

You rarely want to rely on hoisting — clean code reads in the order it runs. A few habits keep it from biting you:

  • Prefer const, then let. Avoid var. This is the single biggest fix. let and const are block-scoped and protected by the TDZ, so accidental early access fails loudly instead of silently. If you’ve internalised the keyword choices from variables and data types, you’re already most of the way there.
  • Declare variables at the top of their scope (or right before first use). Then there’s no gap to misread, and the code reads in execution order.
  • Define functions before you call them. Function declarations technically allow the reverse, but reading code that calls things before they’re defined is harder for humans, even when the engine copes fine.
  • Treat a stray undefined as a clue. If a value is undefined when you expected data, a hoisted var read before its assignment is a classic culprit — check the ordering.

The practical takeaway

You don’t need to memorise the two-phase engine internals to write solid code. Stick to const and let, declare things before you use them, and hoisting essentially stops mattering. The reason to understand it is for the day you read someone else’s var-heavy code, or hit a ReferenceError you didn’t expect — now you’ll know exactly what’s going on.

Wrapping up

Hoisting is the engine registering declarations before running your code, which makes some names usable before their line — but in different ways:

  • var is hoisted and set to undefined, so reading it early returns undefined (no error). It’s also function-scoped, leaking out of blocks.
  • let and const are hoisted but uninitialized, sitting in the temporal dead zone until their line. Accessing them early throws a ReferenceError — a helpful, loud failure.
  • Function declarations are hoisted completely (callable before definition). Function expressions and arrow functions hoist only their variable, so calling them early throws.
  • The practical defence is simple: use const/let, declare before use, define functions before calling them. Do that and hoisting fades into the background where it belongs.

Once hoisting clicks, a lot of “wait, why did that happen?” moments resolve themselves. It’s also a stepping stone to deeper topics like the value of functions and how scope chains resolve names — the kind of detail that turns guesswork into understanding.

Tags:javascriptfrontendhoistingvar let constintermediate
728 × 90Ad Space AvailablePlace your ad here

Related Articles

See All Articles

You Might Also Like

Frontend best practices overview cover
Frontend / Fundamentals

Frontend Best Practices: An Overview

A friendly map of what good frontend really means — semantic HTML, clean CSS, unobtrusive JavaScript, responsive layouts, performance, and progressive enhancement, with pointers to go deeper.

Sep 20, 20268 min read
A Frontend Developer Roadmap cover with the path HTML to CSS to JavaScript
Frontend / Fundamentals

A Frontend Developer Roadmap: What to Learn, and in What Order

A calm, step-by-step learning path for beginners: HTML, CSS, JavaScript, developer tools, responsive and accessible design, a framework, TypeScript, and deployment, with the reasoning behind each step.

Sep 20, 202611 min read
Title card reading Styling and Interactivity over a dark blue ACY Partner background
Frontend / Fundamentals

How Styling and Interactivity Work

A beginner-friendly look at how CSS turns plain HTML into a designed layout, and how JavaScript adds behavior — the structure, style, behavior mental model for building web pages.

Sep 20, 20269 min read
Three front-end layers combining on one web page
Frontend / Fundamentals

How HTML, CSS, and JavaScript Work Together

A hands-on walkthrough for beginners: build one small button, give it structure with HTML, looks with CSS, and behavior with JavaScript, and watch the three layers cooperate on a real page.

Sep 20, 20268 min read
The Frontend Developer Toolkit cover with editor, browser, and CLI motifs
Frontend / Fundamentals

The Frontend Developer Toolkit

A friendly tour of the everyday tools a frontend developer relies on — code editor, browser and DevTools, terminal, version control, package managers, and a dev server — and what each one is actually for.

Sep 20, 202610 min read
Cover illustration for What Frontend Developers Do
Frontend / Fundamentals

What Frontend Developers Do: A Beginner's Guide to the Role

A clear, beginner-friendly look at what frontend developers actually do every day: turning designs into working interfaces, building reusable UI, and caring about responsiveness, accessibility, and speed.

Sep 20, 202610 min read