Closures in JavaScript

DesiEats needs an order counter. Every new order should receive the next number: 1001, 1002, and so on.

Ananya first puts the counter in a global variable. It works, but any part of the app can change it by mistake. Rahul asks a better question: can the counter remember its value without exposing that value to everyone?

The answer is a closure. A closure lets a function remember variables from the place where it was created. It is like giving the order counter a small locked notebook that only its trusted function can open.

What You Will Learn

  • How JavaScript scope controls where variables can be used
  • What a closure remembers
  • How closures preserve private state between calls
  • How function factories create customised functions
  • Why closures inside loops can surprise you
  • When closures are useful and when another design is clearer

What Is a Closure?

A closure is a function together with the variables from its surrounding lexical scope. Lexical scope means that JavaScript decides which variables are available by looking at where code is written.

An inner function can use a variable from an outer function. If the inner function survives after the outer function returns, it keeps access to that variable. That remembered connection is the closure.

Think of a lunchbox. The outer function packs some ingredients. The inner function carries the lunchbox away. Even after the kitchen has finished packing, the inner function can still use what was packed inside.

Why Do We Need Closures?

Closures help when a function needs to remember something safely. Common uses include:

  • Keeping private state
  • Building counters and configurable functions
  • Creating callbacks that need earlier values
  • Storing cached results
  • Keeping a small piece of state close to the logic that owns it

Scope Comes First

Before looking at closures, remember the basic scope rule: a function can read variables from its own scope and from outer scopes.

What just happened?

  • restaurant is in the outer scope, so describeOrder can read it.
  • orderType belongs to describeOrder.
  • Code outside the function cannot read orderType directly.
  • JavaScript looks outward when a variable is not found in the current scope.

This outward lookup is the foundation of closures.

A Function Can Remember an Outer Variable

Now return a function from another function. The outer function finishes, but the returned function still knows the outer variable.

What just happened?

  1. createOrderNumber creates nextNumber with the value 1001.
  2. It returns an inner function that uses nextNumber.
  3. createOrderNumber finishes, but the inner function keeps the connection to nextNumber.
  4. Each call reads the current value and then increases it.

The returned function and nextNumber form a closure. The variable is not copied into a new value on every call. The function keeps access to the same remembered binding.

Closures Preserve Private State

A variable inside a function is not directly available outside that function. A closure lets you expose selected operations without exposing the state itself.

This is similar to a bank counter. You can deposit or withdraw through the approved counter, but you cannot walk into the locked cash room and edit the balance directly.

What just happened?

balance and owner are local variables. The returned methods can use them, but the caller cannot access them as wallet.balance. This pattern creates private state without a public property.

Each Closure Has Its Own State

Every call to the outer function creates a new scope. Therefore, every call can create a separate closure with separate state.

Imagine two DesiEats restaurants. Each restaurant needs its own order count. They should not share one notebook by accident.

kitchenCounter and cateringCounter do not share count. Each invocation of createCounter made a new lexical environment.

Function Factories

A function factory is a function that creates and returns another function. The created function can remember configuration from the factory call.

Think of a kitchen manager preparing different recipe cards. One card applies a ten percent discount, while another applies twenty percent. The worker does not need to remember the percentage separately; the card already contains it.

The two calculators use the same logic but remember different rates. This is one reason closures are useful for reusable, configurable behaviour.

Closures in Loops

Closures become especially important when functions are created inside loops. With let, each loop iteration gets its own binding.

Each callback closes over the binding for its own iteration. Older code often used var, whose function scope caused every callback to observe the final loop value. Prefer let for loop counters in modern JavaScript.

Closures and Memoization

Memoization means caching a function's previous results. A closure can keep a private cache and return an earlier answer when the same input appears again.

The cache is private. Callers only see the result, while the closure remembers previous work. Memoization is most useful when a calculation is expensive and gives the same result for the same input.

Real-World Example: A DesiEats Session

A session should remember a customer and how many orders they have placed, but callers should not change the count directly.

The public API has only two actions: place an order and read a summary. The orderCount variable stays inside the closure, where only those actions can use it.

Tradeoffs

Benefits

  • Closures keep related state and behaviour together.
  • They can create private state without exposing implementation details.
  • They are excellent for callbacks and configurable function factories.
  • Each outer-function call can create independent state.

Costs

  • A closure can keep referenced data alive as long as the closure is reachable.
  • Too many layers of nested functions can make code harder to read.
  • Debugging private state can be less direct than inspecting a normal object.
  • A class may communicate the design more clearly when the object has many operations or a long lifecycle.

Use a closure when the state belongs naturally to a small group of functions. Use a plain object or class when the data model needs a larger, more visible public API.

Common Mistakes and Gotchas

1. Expecting local variables to be public

function createCounter() {
  let count = 0;
  return () => ++count;
}

const counter = createCounter();

// Wrong: count is not a property of counter.
console.log(counter.count); // -> undefined

The closure remembers count, but it does not expose it as a property.

2. Accidentally sharing state

let count = 0;

// Wrong when each counter should be independent.
const first = () => ++count;
const second = () => ++count;

Put the state inside the factory when each created function needs its own state.

3. Capturing a changing outer variable

let discount = 0.1;
const getPrice = price => price - price * discount;

discount = 0.2;
console.log(getPrice(100)); // -> 80

The function reads the current outer variable when it runs. If the value should be fixed, capture it in a factory parameter and avoid mutating that state.

4. Keeping unnecessary data alive

A long-lived closure that references a large object can keep that object in memory. Keep closures small and release references when they are no longer needed.

Code Review Corner

Can you find the problems in this code?

function createOrderTracker() {
  let orders = [];

  return {
    add(order) {
      orders.push(order);
    },

    getOrders() {
      return orders;
    },

    clear() {
      orders = [];
    }
  };
}

const tracker = createOrderTracker();
const visibleOrders = tracker.getOrders();
visibleOrders.push("fake order");
Reveal the review

The closure hides the orders variable, but getOrders returns the actual array. Callers can mutate it without using add. Return a copy instead:

getOrders() {
  return [...orders];
}

The clear method is valid, but replacing the array means any earlier reference returned by a buggy getter would become confusing. A copy keeps the boundary clear.

Interview Questions

What is a closure?

A closure is a function together with access to variables from its lexical scope. The function keeps that access even after the outer function has returned.

What is the output?

function createCounter() {
  let value = 0;
  return () => ++value;
}

const a = createCounter();
const b = createCounter();
console.log(a(), a(), b());

Answer: 1 2 1. Each factory call creates an independent value.

Why can a closure provide private state?

Variables inside the outer function are not directly visible to callers. Returned functions can provide controlled operations that read or change those variables.

Does a closure copy a variable's value?

No. It keeps access to the variable binding. If the variable changes, later calls can observe the changed value.

How does let differ from var in loop closures?

let creates a separate binding for each loop iteration. var is function-scoped, so callbacks created in the loop can all refer to the same final binding.

When might a class be clearer than a closure?

When the object has many related methods, a clear lifecycle, or needs to be extended and inspected as a named type. Closures are often simpler for a small private state machine or function factory.

Practice Challenges

Easy: Order counter

Create createOrderCounter(start) so each call returns the next number.

function createOrderCounter(start) {
  // Return a function that remembers the next number.
}

const nextOrder = createOrderCounter(1001);
console.log(nextOrder()); // -> 1001
console.log(nextOrder()); // -> 1002

Hint: Keep a nextNumber variable inside the outer function.

Medium: Discount factory

Create createDiscount(rate) that returns a function for calculating a discounted price.

function createDiscount(rate) {
  // Return a price calculator.
}

const studentDiscount = createDiscount(0.15);
console.log(studentDiscount(200)); // -> 170

Hint: The returned function should close over rate.

Hard: Private order tracker

Create a tracker with add, count, and getOrders methods. Do not let callers mutate the internal array through getOrders.

function createOrderTracker() {
  // Keep orders private and return three safe operations.
}

const tracker = createOrderTracker();
tracker.add("Masala Dosa");
console.log(tracker.count()); // -> 1

Hint: Return a new array from getOrders with the spread syntax.

Quick Recap

  • Scope determines which variables a function can access.
  • A closure is a function plus access to its surrounding lexical scope.
  • The outer function can finish while the closure keeps its variables alive.
  • Closures are useful for private state and independent counters.
  • Function factories create configured functions.
  • let creates safer per-iteration bindings in loops.
  • Closures can support memoization, but they should not retain unnecessary data.
  • Choose a closure, object, or class based on which design makes the state and API clearest.

What's Next

Closures hide state inside functions. Next, classes and private fields will provide another way to model objects with protected state and shared behaviour.