Scope determines where a variable is accessible in your code. Hoisting is a related but separate behavior: JavaScript processes certain declarations before running any code, which can make variables and functions usable in places that look, at first glance, like they shouldn't work yet. Understanding both together clears up a lot of confusing JavaScript behavior.

Scope: Global, Function, and Block

A variable declared outside any function is in the global scope and can be accessed anywhere. A variable declared inside a function is in function scope — it only exists within that function. let and const also introduce block scope, meaning a variable declared inside { } (an if statement, a loop, etc.) only exists within those braces.

let globalVar = "I'm global";

function demo() {
  let functionVar = "I'm function-scoped";

  if (true) {
    let blockVar = "I'm block-scoped";
    console.log(blockVar); // works here
  }

  // console.log(blockVar); // ReferenceError — blockVar doesn't exist out here
  console.log(functionVar); // still works, same function
}

demo();
console.log(globalVar); // works anywhere

Output:

I'm block-scoped
I'm function-scoped
I'm global

How var Differs from let and const

This is where things get tricky. var is only function-scoped, not block-scoped — it completely ignores if and for block boundaries.

function varExample() {
  if (true) {
    var x = "leaked out of the block";
  }
  console.log(x); // works — var doesn't respect block scope
}

varExample();

Output:

leaked out of the block

This is one of the main reasons modern JavaScript favors let and const — see the full comparison in the var, let, and const tutorial.

Hoisting

JavaScript "hoists" declarations to the top of their scope before execution — but what gets hoisted, and in what state, differs by keyword.

console.log(hoistedVar); // undefined, not an error — declaration was hoisted
var hoistedVar = "value";

sayHi(); // works — function declarations are fully hoisted

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

// console.log(hoistedLet); // ReferenceError: Cannot access before initialization
let hoistedLet = "value";

Output:

undefined
Hi!

var declarations are hoisted and initialized to undefined immediately, so reading them early doesn't crash — it just gives you an unhelpful undefined. Function declarations are hoisted completely, body and all, so you can call them before they appear in the file. let and const are hoisted too, technically, but they stay in an uninitialized temporal dead zone until their declaration line actually runs — accessing them before that throws an error instead of silently returning undefined. Most developers consider this stricter behavior a feature: it surfaces bugs immediately instead of hiding them.

This scoping behavior is also the foundation behind closures, where a function keeps access to variables from the scope it was created in, even after that outer function has finished running.