Tutorials Logic
Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

JavaScript Closures Tutorial - Lexical Scope, Examples

JavaScript Closures

A closure is created when a function remembers variables from the scope where it was created, even after that outer scope has finished running. In simple words, an inner function can keep access to variables from its parent function.

Closures are not a special syntax. They are a natural result of two JavaScript features working together: lexical scope and functions as values. Because functions can be returned, stored in variables, passed as callbacks, and executed later, they may carry their surrounding scope with them.

The Short Definition

A closure is a function bundled together with references to its outer lexical environment. That means the function can access variables that were in scope when the function was created.

Smallest Closure Example
function outer() {
  const message = 'Hello from outer';

  function inner() {
    console.log(message);
  }

  return inner;
}

const savedFunction = outer();
savedFunction(); // Hello from outer

When outer() finishes, its local execution is done. But inner() still needs message, so JavaScript keeps the required variable alive. That remembered access is the closure.

Lexical Scope Comes First

To understand closures, first understand lexical scope. Lexical scope means a variable's scope is decided by where the variable and function are written in the source code, not by where the function is called.

Lexical Scope
const globalName = 'Tutorials Logic';

function outer() {
  const topic = 'Closures';

  function inner() {
    console.log(globalName); // available from global scope
    console.log(topic);      // available from outer scope
  }

  inner();
}

outer();

The inner() function can access topic because it is written inside outer(). The call location does not create the scope. The code location does.

When Does a Closure Happen?

Every function has access to its outer scope while it runs. A closure becomes especially visible when a function continues to use that outer scope later. This usually happens when a function is returned, assigned to a variable, passed as a callback, or used by a timer or event listener.

  • A function is returned from another function.
  • A callback remembers variables from the surrounding function.
  • An event handler remembers state after setup code finishes.
  • A timer function runs later but still sees earlier variables.

Why Closures Matter

Closures are everywhere in JavaScript. They power private state, function factories, callbacks, event handlers, debounce and throttle utilities, memoization, module patterns, and many framework patterns.

  • Preserve values between function calls.
  • Create private variables without global variables.
  • Generate customized functions with factory functions.
  • Keep configuration available to callbacks and handlers.
  • Avoid repeating setup logic across many functions.

Private State with Closures

One of the most useful closure patterns is private state. Variables inside a function are not directly accessible from outside, but returned functions can still read and update them.

Private Counter
function createCounter() {
  let count = 0;

  return {
    increment() {
      count += 1;
      return count;
    },
    decrement() {
      count -= 1;
      return count;
    },
    getValue() {
      return count;
    }
  };
}

const counter = createCounter();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue());  // 2
console.log(counter.count);       // undefined

The count variable is private. Code outside createCounter() cannot read it directly, but the returned methods can access it because they close over the same lexical environment.

Each Closure Gets Its Own State

Every time the outer function runs, JavaScript creates a new lexical environment. That means each closure can have its own independent state.

Independent State
function createCounter(start = 0) {
  let count = start;

  return function increment() {
    count += 1;
    return count;
  };
}

const first = createCounter(0);
const second = createCounter(10);

console.log(first());  // 1
console.log(first());  // 2
console.log(second()); // 11
console.log(second()); // 12

first and second were created by the same function, but they do not share the same count. Each call to createCounter() creates a fresh private variable.

Function Factories

A function factory creates and returns another function. Closures make factories powerful because the returned function remembers the configuration passed to the factory.

Multiplier Factory
function multiplyBy(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

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

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

double() remembers multiplier = 2, while triple() remembers multiplier = 3. This is the same outer function producing different behavior.

Closures in Event Handlers

Event handlers often use closures because setup code runs once, while the handler function runs later. The handler can remember variables from setup time.

Event Handler Closure
function setupButton(button, label) {
  let clicks = 0;

  button.addEventListener('click', function() {
    clicks += 1;
    console.log(`${label} clicked ${clicks} times`);
  });
}

const saveButton = document.querySelector('#save');
setupButton(saveButton, 'Save');

After setupButton() finishes, the click handler still remembers clicks and label. That is why every button can keep its own click count without using global variables.

Closures in Loops

Closures inside loops are a common source of confusion. With var, callbacks share one function-scoped variable. With let, each loop iteration gets a new block-scoped binding.

var vs let in Loops
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log('var:', i);
  }, 100);
}

for (let j = 1; j <= 3; j++) {
  setTimeout(function() {
    console.log('let:', j);
  }, 100);
}

// Output:
// var: 4
// var: 4
// var: 4
// let: 1
// let: 2
// let: 3

The var callbacks all see the same i, which has become 4 after the loop finishes. The let callbacks each close over a different j binding.

Old Fix for var Loop Closures

Before let, developers often used an IIFE to create a new scope for each loop iteration. You will still see this pattern in old codebases and interview questions.

IIFE Loop Fix
for (var i = 1; i <= 3; i++) {
  (function(currentValue) {
    setTimeout(function() {
      console.log(currentValue);
    }, 100);
  })(i);
}

// Output:
// 1
// 2
// 3

The IIFE receives the current value of i as currentValue. Each timeout closes over a different function call's variable.

Closures with Timers

Timers show closures clearly because the callback runs after the outer function has completed.

Timer Closure
function remindUser(name) {
  const message = `Hi ${name}, take a short break.`;

  setTimeout(function() {
    console.log(message);
  }, 2000);
}

remindUser('Asha');

The callback passed to setTimeout() remembers message, even though remindUser() is no longer running when the timer fires.

Debounce with Closures

Debounce waits until repeated calls stop for a certain time. It is useful for search boxes, resize handlers, and input validation. Closures keep track of the timer ID between calls.

Debounce Utility
function debounce(callback, delay) {
  let timerId;

  return function(...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      callback.apply(this, args);
    }, delay);
  };
}

const search = debounce(function(query) {
  console.log('Searching for:', query);
}, 300);

search('j');
search('ja');
search('jav');
search('java'); // only this call runs after 300ms

The returned function remembers timerId. Every call clears the previous timer and starts a new one.

Throttle with Closures

Throttle limits a function so it runs at most once during a time window. It is useful for scroll, mousemove, and resize events.

Throttle Utility
function throttle(callback, delay) {
  let waiting = false;

  return function(...args) {
    if (waiting) return;

    callback.apply(this, args);
    waiting = true;

    setTimeout(() => {
      waiting = false;
    }, delay);
  };
}

const onScroll = throttle(function() {
  console.log('Scroll handler ran');
}, 500);

window.addEventListener('scroll', onScroll);

The returned function remembers waiting. While waiting is true, extra calls are ignored.

Memoization with Closures

Memoization stores results so expensive work does not need to run again for the same input. A closure keeps the cache private.

Memoization
function memoize(fn) {
  const cache = new Map();

  return function(value) {
    if (cache.has(value)) {
      console.log('From cache');
      return cache.get(value);
    }

    console.log('Calculated');
    const result = fn(value);
    cache.set(value, result);
    return result;
  };
}

const square = memoize(n => n * n);

console.log(square(5)); // Calculated, 25
console.log(square(5)); // From cache, 25

The cache variable is not global. Only the returned function can access it.

Module Pattern with Closures

Before ES modules became common, developers used closures to create modules with private variables and a public API. This pattern is still useful for understanding older JavaScript code.

Module Pattern
const cartModule = (function() {
  const items = [];

  return {
    addItem(name, price) {
      items.push({ name, price });
    },
    getTotal() {
      return items.reduce((total, item) => total + item.price, 0);
    },
    getCount() {
      return items.length;
    }
  };
})();

cartModule.addItem('Keyboard', 50);
cartModule.addItem('Mouse', 20);

console.log(cartModule.getCount()); // 2
console.log(cartModule.getTotal()); // 70
console.log(cartModule.items);      // undefined

Closures and Async Code

Closures are common in asynchronous JavaScript because callbacks, promises, and async functions often run after surrounding code has moved on.

Async Closure
function createUserLoader(apiUrl) {
  return async function loadUser(id) {
    const response = await fetch(`${apiUrl}/users/${id}`);
    return response.json();
  };
}

const loadUser = createUserLoader('https://api.example.com');

// loadUser remembers apiUrl.
loadUser(42).then(user => {
  console.log(user);
});

The returned loadUser() function remembers apiUrl. You do not need to pass the base URL every time.

Closure Does Not Copy Values

A common misconception is that closures copy variable values. In reality, closures keep access to variables. If the variable changes, the closure sees the latest value.

Reference to Variable
function createLogger() {
  let message = 'First message';

  function log() {
    console.log(message);
  }

  message = 'Updated message';
  return log;
}

const logger = createLogger();
logger(); // Updated message

log() does not store a frozen copy of 'First message'. It keeps access to the variable message, which was updated before the function was returned.

Closures and Memory

Closures keep variables alive as long as the closure is reachable. This is useful, but it also means large objects can stay in memory if a closure still references them. In most apps this is not a problem, but it matters for long-lived event listeners, caches, and large data structures.

Avoid Holding Large Data
function attachHandler(button, largeData) {
  function handleClick() {
    console.log('Clicked');
    // If largeData is referenced here, it stays alive.
  }

  button.addEventListener('click', handleClick);

  return function cleanup() {
    button.removeEventListener('click', handleClick);
  };
}

const cleanup = attachHandler(document.querySelector('button'), []);

// Later, when the handler is no longer needed:
cleanup();

Remove long-lived event listeners when they are no longer needed, especially in single-page applications where pages and components can mount and unmount without a full reload.

Closures vs Classes

Closures and classes can both manage state. Closures are great for small private pieces of state and factory-style APIs. Classes are useful when you need many related methods, inheritance, or a familiar object-oriented structure.

Closure vs Class
// Closure style
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count
  };
}

// Class style
class Counter {
  #count = 0;

  increment() {
    return ++this.#count;
  }

  value() {
    return this.#count;
  }
}

Modern JavaScript classes support private fields with #, but closure-based privacy is still common and useful, especially for small utilities.

Interview Explanation

If you are asked "What is a closure?" in an interview, give a short definition and then explain with a small example.

Interview Answer
function makeGreeting(name) {
  return function() {
    return `Hello, ${name}`;
  };
}

const greetAsha = makeGreeting('Asha');
console.log(greetAsha()); // Hello, Asha

A strong interview answer: "A closure is when a function keeps access to variables from its outer lexical scope even after that outer function has returned. In this example, the returned function still remembers name."

Common Closure Use Cases

Use case What the closure remembers
Private counter The current count value
Function factory Configuration such as multiplier, prefix, or API URL
Event handler Setup state, DOM references, labels, or counters
Debounce The current timer ID
Throttle Whether the function is waiting
Memoization A private cache

Common Beginner Mistakes

  • Thinking closures copy values instead of keeping access to variables.
  • Forgetting that var is function-scoped in loop callbacks.
  • Creating hidden state when a simple parameter would be clearer.
  • Keeping large objects alive through long-lived callbacks.
  • Using closures for everything when a class, module, or plain object would be easier to read.

Summary

Closures happen when a function keeps access to variables from its outer lexical scope. They let JavaScript functions remember state, create private variables, produce customized functions, and support callbacks that run later. Once you understand lexical scope and function values, closures become less mysterious: a closure is simply a function carrying the variables it still needs.

Key Takeaways
  • A closure is a function that remembers variables from its outer lexical scope.
  • Closures happen because JavaScript uses lexical scope and functions can be passed around as values.
  • Returned functions, callbacks, timers, and event handlers commonly create visible closures.
  • Closures are useful for private state, factories, debounce, throttle, memoization, and module patterns.
  • let creates a new loop binding for each iteration, while var shares one function-scoped binding.
  • Closures keep referenced variables alive, so be careful with long-lived handlers and large captured objects.
Common Mistakes to Avoid
WRONG Thinking a closure stores a fixed copy of a variable value
RIGHT A closure keeps access to the variable itself
If the outer variable changes before the closure runs, the closure sees the updated value.
WRONG Using var in async loop callbacks
RIGHT Use let for a fresh binding per iteration
let makes loop closure behavior match what most beginners expect.
WRONG Leaving event listeners alive forever
RIGHT Remove listeners when they are no longer needed
Long-lived event handlers can keep closed-over variables in memory.

Frequently Asked Questions


Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.