Academic Block

JavaScript Scope and Closures Get Certified

Scope determines where variables and functions are accessible in your code. Closures are a powerful consequence of how scope works in JavaScript — they let functions “remember” the environment in which they were created.

In this lesson, you will learn about global, function, and block scope, the scope chain, variable shadowing, how closures work, and common patterns and pitfalls.

Global Scope

Variables declared outside any function or block belong to the global scope and can be accessed from almost anywhere in the program.

What this code does: A variable declared at the top level is global. Both the function and the code outside it can read and use appName.

const appName = "Task Manager";

function showAppName() {
    console.log("App:", appName);
}

showAppName();
console.log("Still accessible:", appName);

Function Scope

Variables declared inside a function (with var, let, or const) are only accessible within that function.

What this code does: Variables declared inside a function are local to that function. Trying to access secret outside the function causes a ReferenceError.

function hideSecret() {
    const secret = "only visible inside";
    console.log("Inside:", secret);
}

hideSecret();

try {
    console.log(secret);
} catch (err) {
    console.log("Error:", err.message);
}

Block Scope

let and const are block-scoped. A block is any code wrapped in curly braces { } (if statements, loops, etc.).

What this code does: let and const respect block scope. The variable declared inside the if block is not accessible outside it.

if (true) {
    let message = "I only exist inside this block";
    console.log(message);
}

try {
    console.log(message);
} catch (err) {
    console.log("Error:", err.message);
}

var is function-scoped (not block-scoped), which is why let and const are generally preferred.

The Scope Chain

When JavaScript looks up a variable, it checks the current scope first, then works outward through each enclosing scope until it finds the variable (or runs out of scopes and throws an error). This is called the scope chain.

What this code does: Inner functions can access variables from outer scopes (the scope chain). printReceipt reads both the global storeName and the function-local orderId.

const storeName = "Downtown Books";

function processOrder() {
    const orderId = "A123";

    function printReceipt() {
        console.log("Store:", storeName);
        console.log("Order:", orderId);
    }

    printReceipt();
}

processOrder();

printReceipt can access orderId from its parent function and storeName from the global scope, because each inner scope has access to variables in the scopes surrounding it.

Variable Shadowing

If a variable in an inner scope has the same name as one in an outer scope, the inner variable “shadows” the outer one within that scope.

What this code does: An inner variable can shadow an outer one with the same name. Inside the function, mode is "dark"; outside, the original "light" is unchanged.

const mode = "light";

function toggleTheme() {
    const mode = "dark";
    console.log("Inside function:", mode);
}

toggleTheme();
console.log("Outside function:", mode);

What Is a Closure?

A closure is formed when a function “remembers” the variables from the scope it was created in, even after that outer scope has finished executing.

What this code does: A closure is a function that remembers variables from the scope where it was created. Each greeter keeps its own greeting even after makeGreeter has finished running.

function makeGreeter(greeting) {
    return function (name) {
        console.log(greeting + ", " + name + "!");
    };
}

const greetFormally = makeGreeter("Good day");
const greetCasually = makeGreeter("Hey");

greetFormally("Michael");
greetCasually("Michael");

Even though makeGreeter has already finished running by the time greetFormally is called, the inner function still remembers the value of greeting from when it was created.

Using Closures to Create Private State

Closures are commonly used to keep a variable private, only accessible through functions that were created alongside it.

What this code does: Closures can create private state. count is hidden inside createCounter; only the returned methods can change or read it. Each counter is independent.

function createCounter() {
    let count = 0;

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

const counter = createCounter();
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.increment());
counter.reset();
console.log(counter.increment());

The variable count cannot be accessed or modified directly from outside createCounter — the only way to interact with it is through the returned increment and reset functions.

A Classic Pitfall: Closures Inside Loops

A well-known bug happens when creating closures inside a loop using var, since var is function-scoped rather than block-scoped.

What this code does: Classic loop-closure bug with var: all callbacks share the same i, which ends up as 4. Every callback prints 4.

const callbacks = [];

for (var i = 1; i <= 3; i++) {
    callbacks.push(function () {
        console.log("var version:", i);
    });
}

callbacks[0]();
callbacks[1]();
callbacks[2]();

Because there is only one shared i, every callback ends up referencing its final value after the loop finishes, printing 4 three times.

What this code does: Using let in the loop creates a new binding per iteration. Each callback captures its own i and prints 1, 2, and 3 correctly.

const callbacks = [];

for (let i = 1; i <= 3; i++) {
    callbacks.push(function () {
        console.log("let version:", i);
    });
}

callbacks[0]();
callbacks[1]();
callbacks[2]();

Using let instead creates a new block-scoped i for each iteration, so each closure correctly captures its own separate value.

Practical Example

Let’s use a closure to build a simple rate limiter that only allows an action a limited number of times.

What this code does: A practical closure: the returned function remembers attemptsUsed and maxAttempts. Each call updates private state until the limit is reached.

function createLimiter(maxAttempts) {
    let attemptsUsed = 0;

    return function () {
        if (attemptsUsed >= maxAttempts) {
            return "Limit reached. Try again later.";
        }
        attemptsUsed++;
        return "Attempt " + attemptsUsed + " of " + maxAttempts + " allowed.";
    };
}

const loginAttempt = createLimiter(3);
console.log(loginAttempt());
console.log(loginAttempt());
console.log(loginAttempt());
console.log(loginAttempt());

Scope Types Overview

Scope Type Created By Accessible From
Global scope Code outside any function or block Anywhere in the program
Function scope var, let, const inside a function That function and any nested functions
Block scope let, const inside { } That block and any nested blocks

Common Mistakes With Scope and Closures

  • Using var inside loops that create closures, leading to every closure sharing the same final value.
  • Assuming a variable declared inside a function or block is accessible outside of it.
  • Accidentally shadowing an outer variable with an inner one of the same name, causing confusing bugs.
  • Overusing global variables, which can be modified from anywhere and are harder to track down when something goes wrong.
  • Assuming multiple calls to the same closure-creating function share the same private variables, when each call actually creates independent state.

Best Practices for Scope and Closures

  • Declare variables in the smallest scope where they are actually needed.
  • Prefer let and const over var to take advantage of block scope.
  • Use closures intentionally to create private state instead of exposing internal variables globally.
  • Avoid unnecessary global variables, which can be accidentally overwritten by unrelated code.
  • Give shadowed variables different names when possible, to keep code easier to follow.

Quick Summary

  • Global scope is accessible everywhere; function scope is limited to a single function; block scope is limited to a single block.
  • The scope chain lets inner scopes access variables from surrounding outer scopes.
  • A closure is a function that remembers variables from the scope where it was created, even after that scope has finished running.
  • Closures are commonly used to create private, protected state.
  • Each call to a function that creates a closure produces its own independent set of variables.
  • Using let instead of var in loops avoids a classic closure bug.

Try It Yourself

Modify the code below. Try creating a second, independent bank account, and see how each one keeps track of its own balance separately.

What this code does: Practice example: a bank account with private balance via closure. Try creating a second account and see that each keeps its own independent balance. Click Test My Code to experiment.

function createAccount(startingBalance) {
    let balance = startingBalance;

    return {
        deposit: function (amount) {
            balance += amount;
            return balance;
        },
        getBalance: function () {
            return balance;
        }
    };
}

const account = createAccount(100);
console.log(account.deposit(50));
console.log(account.getBalance());
Tip: A helpful way to think about closures: a function doesn’t just remember its own code — it remembers the entire neighborhood of variables it was born into, and it keeps that neighborhood alive for as long as the function itself exists.

Understanding scope and closures is essential before moving on to the this keyword, higher-order functions, and more advanced JavaScript concepts.

🧪 Test Your JavaScript Code

Edit the code on the left and click “Run Code” to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click “Run Code” to see the output here.

Ctrl + Enter to run code Esc to close