Closures and Scope

Reviewed & published by Brayan K

A clear, in-depth guide to closures in JavaScript — with worked examples, common pitfalls, and a practice quiz.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

🎒 Real-World Analogy: A closure is like a backpack that remembers:

INTRODUCTION — Why Closures Matter More Than Anything Else in JavaScript

Closures are the heart and soul of JavaScript.

Closure Use CaseWhat It DoesReal Example
Private variablesHide data from outside accessCounter that can't be reset
Function factoriesCreate customized functionsmakeMultiplier(5)
Event handlersRemember context when clickedButton click with ID
React hooksuseState, useEffect internalsComponent state

Every professional JavaScript developer — especially those working in React, Node.js, or complex web apps — uses closures every single day, often without noticing.

By the end of this lesson, closures will feel completely natural.

SECTION 1 — Understanding SCOPE (Global, Function, Block, Lexical)

Before closures make sense, you MUST understand scope.

Scope = where variables can be accessed.

JavaScript uses Lexical Scope, meaning:

📌 Variables are resolved based on WHERE the code was written, not where it is executed.

1. Global Scope

Anything declared outside a function is global.

Accessible everywhere. Dangerous for large apps.

2. Function Scope

var variables live inside the function only.

Function boundaries matter a lot in closures.

3. Block Scope (let / const)

Block scoping is essential for fixing closure bugs (you'll see later).

4. Lexical Scope (Most Important)

Inner functions access variables declared where they were written.

Lexical scope is the engine behind closures.

SECTION 2 — What Is a Closure? (Simple Definition)

A function that "remembers" variables from its outer scope — even after the outer function has returned.

This is the single most important concept in JavaScript.

The Classic Example:

Even though outer() has finished executing, the inner function keeps access to:

Closures keep data alive.

SECTION 3 — The Real Superpower: Private Variables

JavaScript has no private keyword (until ES2020 classes). Before that, closures were the ONLY way to hide data.

Example: Private Counter

This is true encapsulation — impossible with global variables.

SECTION 3b — Worked Example: A Wallet With a Private Balance

Here is the counter idea turned into something you would actually ship. Read the whole program first — every non-obvious line has a comment telling you what it does and why — then press Run and check the output matches the comments.

The thing to watch for: createWallet finishes running on the line where you call it, yet balance is still alive several lines later. That is the closure doing its job.

// -- WORKED EXAMPLE - a closure you can watch working --

function createWallet(ownerName, startingBalance) {
  // 'balance' is PRIVATE: it lives inside createWallet and nothing outside
  // this function can reach it directly. Only the functions below can.
  let balance = startingBalance;

  // Each function below is written INSIDE createWallet, so each one packs a
  // "backpack" holding ownerName and balance. That backpack is the closure.
  return {
    deposit(amount) {
      balance = balance + amount;              // reaches into the backpack
      return ownerName + " deposited " + amount + ". Balance: " + balance;
    },
    withdraw(amount) {
      if (amount > balance) {                  // guard: never go negative
        return "Declined - " + ownerName + " only has " + balance;
      }
      balance = balance - amount;
      return ownerName + " withdrew " + amount + ". Balance: " + balance;
    },
    getBalance() {
      return balance;                          // read-only view of the private value
    }
  };
}

// createWallet() runs once and FINISHES on this line...
const wallet = createWallet("Ada", 100);
// ...but 'balance' stays alive, because the three returned functions remember it.

console.log(wallet.deposit(50));     // Ada deposited 50. Balance: 150
console.log(wallet.withdraw(30));    // Ada withdrew 30. Balance: 120
console.log(wallet.withdraw(999));   // guard fires, balance is left alone

// The private variable really is unreachable from outside:
console.log(wallet.balance);         // undefined - there is no 'balance' property
console.log(wallet.getBalance());    // 120 - the only legitimate way to read it

// Every call to createWallet builds a BRAND NEW backpack, not a shared one:
const wallet2 = createWallet("Grace", 10);
console.log(wallet2.getBalance());   // Grace's own balance
console.log(wallet.getBalance());    // Ada's is untouched

// Expected output:
// Ada deposited 50. Balance: 150
// Ada withdrew 30. Balance: 120
// Declined - Ada only has 120
// undefined
// 120
// 10
// 120

Try breaking it on purpose: change let balance to const balance and run again. You get TypeError: Assignment to constant variable. — proof that deposit really is writing to the outer variable, not to a copy.

SECTION 4 — Function Factories (Closures Generating Functions)

Closures let you preload data into functions.

Make a Function That Remembers a Number (Multiplier):

🎯 Your Turn — Finish the Closures

Everything below is written for you except two blanks. Both blanks are about the one idea this lesson teaches: an inner function reaching a variable from the function that created it. Fill them in and press Run.

// 🎯 YOUR TURN - fill in the blanks marked ___

// PART 1 - a function factory
function makeLogger(label) {
  // The function you return is written INSIDE makeLogger, so it can remember
  // 'label' forever - even after makeLogger has finished running.
  return function (message) {
    // 👉 1) Replace ___ with the variable the outer function remembers.
    return "[" + ___ + "] " + message;
  };
}

const errorLog = makeLogger("ERROR");   // this logger remembers "ERROR"
const infoLog  = makeLogger("INFO");    // a separate backpack, remembering "INFO"

console.log(errorLog("Disk full"));
console.log(infoLog("Backup finished"));

// PART 2 - a private counter
function createCounter() {
  // 👉 2) Replace ___ with the keyword that makes 'hits' private to this
  //       function AND lets it be reassigned. (Not const. Not var.)
  ___ hits = 0;

  return {
    hit() {
      hits = hits + 1;   // reaches into the closure and updates it
      return hits;
    },
    total() {
      return hits;       // the only way to read the private value
    }
  };
}

const counter = createCounter();
counter.hit();
counter.hit();
counter.hit();

console.log("Total hits:", counter.total());
console.log("Direct access:", counter.hits);   // stays hidden

// ✅ Expected output:
// [ERROR] Disk full
// [INFO] Backup finished
// Total hits: 3
// Direct access: undefined

Stuck on blank 2? The last line is the clue: counter.hits must print undefined. If you pick a keyword that puts hits on the returned object, that line would print 3 instead.

SECTION 5 — Closures and Event Handlers

Event handlers, timeouts, and interactive websites heavily rely on closures.

Example:

Even after setupButton finishes, the inner arrow function still knows:

That's why closures are essential for UI programming.

SECTION 6 — Closures + Async Code (Critical for Modern JS)

Closures keep variables alive during async operations.

Even though loadUser has returned long before the API finishes, the .then() callback remembers prefix.

This is EXACTLY how:

all retain access to old variables.

SECTION 7 — Memoization with Closures (Speed Boost Technique)

Memoization = caching results so future calls are instant.

Closures make memoization easy.

SECTION 8 — SUMMARY & CHEAT SHEET

const global = 1;
function() { var x = 1; }
if (true) { let x = 1; } // x not accessible outside
function outer() { const x = 1; function inner() { console.log(x); } }

📋 Quick Reference — Closures

ConceptPattern
Basic Closurefunction outer() { return function inner() {} }
Private Datalet count = 0; return { inc: () => ++count }
FactorymakeAdder(5)(10) // 15
MemoizationCache results inside closure scope
Event Handlerbtn.onclick = () => log(id)

🏆 Mini-Challenge: Write once()

No blanks this time — just a brief and an outline. Write a function called once(fn). It takes a function and hands you back a new function that runs fn only the first time it is called. Every later call returns the value from that first run without calling fn again.

This is a real utility — libraries use it for one-time setup, and it is impossible to write without a closure, because the "have I run yet?" flag has to survive between calls while staying hidden from everyone else.

// 🎯 MINI-CHALLENGE: write once(fn)
//
// Outline - no logic filled in, that part is yours:
//
// function once(fn) {
//   1. create a private flag 'called', starting as false
//   2. create a private 'result' to hold the first return value
//   3. return a function that accepts ...args
//   4.    if called is still false: set called = true and store fn(...args) in result
//   5.    return result   (the same value every single time)
// }
//
// Until you have written once(), running this throws
// "ReferenceError: once is not defined" - that is expected, not a bug.

// your code here


// --- test harness: do not change anything below this line ---
let runs = 0;
const setup = once(function (name) {
  runs = runs + 1;                     // counts how many times the REAL work happened
  return "Configured " + name;
});

console.log(setup("app"));
console.log(setup("app"));
console.log(setup("something else"));  // ignored - the first result is reused
console.log("times the real function ran:", runs);

// ✅ Expected output:
// Configured app
// Configured app
// Configured app
// times the real function ran: 1

Note the third line of expected output: setup("something else") still prints Configured app. That is the point — the argument is ignored because fn never runs a second time.

Lesson Complete — Closures & Scope!

You've mastered one of the hardest concepts in JavaScript. Understanding closures puts you in the top tier of JavaScript developers.

Practice quiz

What is a closure in JavaScript?

  • A way to close a browser window
  • A function with no parameters
  • A function that remembers variables from its outer scope even after the outer function has returned
  • A loop that runs forever

Answer: A function that remembers variables from its outer scope even after the outer function has returned. The lesson defines a closure as a function that remembers variables from its outer scope, even after the outer function has returned.

What kind of scope does JavaScript use to resolve variables?

  • Lexical scope
  • Dynamic scope
  • Random scope
  • Block-only scope

Answer: Lexical scope. JavaScript uses lexical scope: variables are resolved based on WHERE the code was written, not where it is executed.

Given: function outer() { const name = 'Boopie'; function inner() { console.log(name); } return inner; } outer()(); What does this log?

  • undefined
  • ReferenceError
  • null
  • 'Boopie'

Answer: 'Boopie'. Through lexical scope, inner() accesses name from where it was written, so calling outer()() logs 'Boopie'.

In the createCounter example, what does counter.count return after two increments?

  • 2
  • undefined
  • 0
  • 1

Answer: undefined. count is a private variable trapped in the closure, so counter.count returns undefined; only the returned methods can reach it.

Which statement about a 'var' variable declared inside a function is correct?

  • It lives inside the function only (function scope)
  • It is global
  • It is block scoped to the nearest if
  • It cannot be reassigned

Answer: It lives inside the function only (function scope). var variables live inside the function only, which is why console.log(x) outside the function throws an error.

After this classic loop with var: var fns = []; for (var i = 0; i < 3; i++) { fns.push(() => i); } What do fns[0](), fns[1](), fns[2]() return?

  • 0, 1, 2
  • 0, 0, 0
  • 3, 3, 3
  • undefined, undefined, undefined

Answer: 3, 3, 3. All callbacks share the same var i, which is 3 after the loop ends, so every call returns 3.

If you change the loop to use 'let' instead of 'var': for (let i = 0; i < 3; i++) { fns.push(() => i); } What do the three functions return?

  • 3, 3, 3
  • 0, 1, 2
  • undefined
  • 1, 2, 3

Answer: 0, 1, 2. let creates a fresh binding of i per iteration, so each closure captures its own value: 0, 1, 2.

In makeMultiplier, given const triple = makeMultiplier(3), what does triple(10) return?

  • 13
  • 3
  • 10
  • 30

Answer: 30. The returned function closes over factor = 3, so triple(10) computes 10 * 3 = 30.

What is the purpose of the cache object inside the memoize closure?

  • To delete old functions
  • To store results so repeated calls with the same arguments return instantly
  • To make the code run slower
  • To convert numbers to strings permanently

Answer: To store results so repeated calls with the same arguments return instantly. Memoization caches results in the closure's cache object so future calls with the same arguments are instant.

Why can event handlers and async callbacks (like .then or setTimeout) still access variables from a function that already returned?

  • Because JavaScript copies the whole program into them
  • Because the variables become global automatically
  • Because closures keep those outer variables alive after the outer function returns
  • Because async code disables scope

Answer: Because closures keep those outer variables alive after the outer function returns. Closures keep the outer scope's variables alive, so handlers and async callbacks (e.g., .then remembering prefix) still access them after the outer function returns.

Continue this course