JavaScript Scope and Closures: Where Variables Live and How They Remember

Scope decides which variables your code can see; closures let a function remember the variables it grew up with. Master both and a huge chunk of JavaScript stops feeling like magic.

Published August 5, 202610 min readBy ACY Partner Indonesia
JavaScript scope and closures — nested boxes of variables and a function that remembers
300 × 250Ad Space AvailablePlace your ad here

Two questions sit underneath an enormous amount of JavaScript behavior: where can a piece of code read a given variable, and how long does that variable stay alive? The answer to the first is scope. The answer to the second — once you mix functions into it — leads straight to closures, one of the features people find most confusing right up until the moment it clicks.

The good news is that neither idea is hard once you stop memorizing rules and start picturing what actually happens. By the end of this article you’ll be able to look at a snippet, trace which variables each line can reach, and explain why a counter you wrote three lines ago still remembers its own private number. We’ll lean on what you already know about functions and variables, so if those feel shaky, skim them first.

What scope actually means

Scope is the set of variables that are visible — reachable by name — from a given spot in your code. Not every variable is available everywhere. JavaScript draws boundaries, and a variable only exists inside the boundary where it was declared.

Think of it like rooms in a building. If you stand in a small room, you can see everything in that room and everything in the larger rooms surrounding it — but someone in the next room over can’t see what’s on your desk. Code works the same way: an inner area can look outward, but the outer area can’t peek inward.

There are three kinds of scope worth naming:

  • Global scope — declared outside every function and block. Visible everywhere.
  • Function scope — declared inside a function. Visible only within that function.
  • Block scope — declared inside a { } block (an if, a for, or just a bare pair of braces). Visible only within that block. This one applies to let and const, but not to var.
const appName = "ACY Partner Indonesia"; // global

function greet() {
  const message = "Welcome"; // function scope
  console.log(message);      // ✅ works
  console.log(appName);      // ✅ works — can see outward to global
}

greet();
console.log(appName);   // ✅ "ACY Partner Indonesia"
console.log(message);   // ❌ ReferenceError: message is not defined

message lives inside greet. The moment you try to read it from the outside, JavaScript has no idea what you’re talking about — that name simply doesn’t exist out there.

Block scope: let and const vs var

This is the one piece of scope history you genuinely need to know, because old code and new code behave differently. Before 2015, var was the only way to declare a variable, and var ignores block boundaries entirely. It only respects function scope.

function check() {
  if (true) {
    var x = 10;   // var ignores the { } block
    let y = 20;   // let is confined to the block
  }
  console.log(x);   // ✅ 10  — var leaked out of the if-block
  console.log(y);   // ❌ ReferenceError — let stayed inside
}

Look closely: x and y were declared on adjacent lines inside the same if. Yet x escaped the block and y didn’t. That’s the whole var problem in one example — it’s loose about where it lives, which leads to bugs that are hard to spot.

Default to const, reach for let, avoid var

In modern JavaScript, declare everything with const first. If you genuinely need to reassign the variable later, switch that one to let. You almost never need var — its block-ignoring behavior causes more surprises than it solves. Reserving var for “never, unless I’m maintaining old code” is a habit that quietly removes a whole category of bugs from your work.

The classic var-in-a-loop trap

The most famous victim of var’s loose scoping is a loop that creates functions. Because every iteration shares the same var i, all the functions end up reading the final value:

// With var — all three print 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// prints: 3, 3, 3

// With let — a fresh i per iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// prints: 0, 1, 2

With let, each turn of the loop gets its own brand-new i, so each function captures a different number. This single difference is reason enough to leave var behind. (Don’t worry about why the setTimeout runs later for now — what matters is which i each function sees.)

The scope chain: how JavaScript finds a variable

When your code uses a variable, JavaScript needs to figure out which one you mean. It does this by walking the scope chain: it looks in the current scope first, and if the name isn’t there, it steps outward to the surrounding scope, then outward again, all the way up to global. The first match wins. If it reaches global and still finds nothing, you get a ReferenceError.

const level = "global";

function outer() {
  const level = "outer";

  function inner() {
    console.log(level);  // "outer"
  }

  inner();
}

outer();

Inside inner, JavaScript looks for level. It’s not declared in inner, so it steps out to outer, finds level = "outer" there, and stops. The global level never gets a look, because the search stopped at the first match. This outward-only search is the engine behind everything else in this article.

The search only goes outward, never inward

A function can read variables from the scopes that enclose it, but never from a scope nested inside it, and never from a sibling function. If inner defines a variable, outer cannot see it. The chain is a one-way street pointing out toward global — keep that direction in mind and most “why is this undefined?” puzzles solve themselves.

Closures: functions that remember

Here’s where it all comes together. A closure is what you get when a function holds on to the variables from the scope where it was created, even after that outer scope has finished running. The function “closes over” those variables and carries them along like a backpack.

That sounds abstract, so let’s see it:

function makeGreeter(name) {
  // `name` lives in makeGreeter's scope
  return function () {
    console.log("Hello, " + name + "!");
  };
}

const greetJohn = makeGreeter("John Doe");
const greetJane = makeGreeter("Jane Doe");

greetJohn();   // "Hello, John Doe!"
greetJane();   // "Hello, Jane Doe!"

Stop and notice something strange. By the time you call greetJohn(), the makeGreeter function has long since finished — it ran, returned, and exited. Normally, when a function exits, its local variables are gone. Yet the inner function still knows that name is "John Doe". That’s the closure: the returned function kept a live link to the name it was born with, so the variable survived past the death of its parent.

And because each call to makeGreeter creates a fresh scope, greetJohn and greetJane remember different names. They don’t share state — each closure has its own private copy.

A private counter (the closure you’ll actually use)

The textbook example, and a genuinely useful pattern, is a counter that keeps its count hidden from the rest of your code:

function createCounter() {
  let count = 0; // private — nothing outside can touch it directly

  return {
    increment() {
      count++;
      return count;
    },
    reset() {
      count = 0;
    },
  };
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.increment());  // 3
counter.reset();
console.log(counter.increment());  // 1

count is locked away inside createCounter. There is no counter.count to read or accidentally overwrite from the outside — the only way to change it is through the increment and reset methods you were given. The closure gives you data privacy: a value that persists between calls but can only be touched through a controlled door. This is the foundation of countless real patterns, from caching to modules to event handlers that remember their setup.

A closure captures the variable, not a snapshot

This catches people out: a closure holds a live reference to the variable, not a frozen copy of its value at creation time. If the variable changes later, the closure sees the new value. That’s exactly why the count above keeps climbing across calls instead of resetting to its starting value — the closure and the original scope are looking at the same count, not two separate ones.

Why closures matter in real code

Closures aren’t a party trick — you’re already using them constantly, often without naming them. A few places they show up every day:

  • Event handlers that need to remember something from when they were set up:
function setupButton(label) {
  const button = document.querySelector("#save");
  button.addEventListener("click", () => {
    // closes over `label` — still available when the click happens later
    console.log("Clicked: " + label);
  });
}
  • Function factories — building specialized functions from a general one:
function multiplyBy(factor) {
  return (n) => n * factor;
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5));   // 10
console.log(triple(5));   // 15

double permanently remembers factor = 2; triple remembers factor = 3. Same factory, two functions with two different memories baked in. Once you spot this pattern, you’ll see it all over real codebases — it’s a clean way to configure behavior up front.

A common gotcha: leaking globals

One last trap that ties scope back to everyday bugs. If you assign to a variable you never declared, in non-strict code JavaScript quietly creates it as a global — far outside the scope you meant it to live in:

function setup() {
  config = { ready: true };  // ❌ no let/const/var — becomes a global!
}

setup();
console.log(config.ready);   // true — config leaked into global scope

You probably intended config to be local to setup. Instead it’s now visible everywhere, ready to collide with anything else named config. Always declare your variables, and put "use strict"; at the top of your files — strict mode turns this silent mistake into a loud ReferenceError so you catch it immediately.

Wrapping up

You now understand the two ideas that quietly govern how JavaScript variables behave:

  • Scope is where a variable is visible. There’s global, function, and block scope. let and const respect block boundaries; var only respects function scope and leaks out of blocks — so default to const, use let when you must reassign, and skip var.
  • JavaScript finds variables by walking the scope chain — from the current scope outward toward global, taking the first match. The search never goes inward or sideways.
  • A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished. It captures the variable itself, not a snapshot of its value.
  • Closures give you private state (the counter pattern), power event handlers and function factories, and are already running throughout your code.
  • Always declare your variables to avoid accidentally leaking globals; strict mode helps catch the mistake.

Scope and closures are the kind of topic that, once it clicks, makes a dozen other behaviors suddenly make sense. A natural companion to this is the this keyword — another rule about context that confuses people for the same reason, and the subject of the next article. With scope and closures under your belt, you’re ready for it.

Tags:javascriptfrontendscopeclosuresintermediate
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