JavaScript Closures Explained: Scope, Examples & Interview Tips

Introduction

Today, we’ll explore what closure is through practical examples—putting it under the microscope. Let’s get started!

What is Closure?

In JavaScript, we can define a closure as: A closure is a function that retains access to its lexical scope (i.e., variables from its surrounding context) even after the outer function has finished executing. That’s all a closure is! I know this might sound confusing at first, but don’t worry, I’ll break it down and walk you through it step by step so you can understand it more easily.

Let’s take a closer look at the definition:

A closure is a function that retains access to its lexical scope (i.e., variables from its surrounding context) even after the outer function has finished executing.

What does this mean?

Suppose we create a function called a. Inside a, we define another function called b, which accesses a variable declared in a. If we return function b from a, the returned function b becomes a closure because it retains access to the variables from function a even after function a has finished executing.

Before we dive deeper into why this happens, let’s look at a simple example:

JavaScript

copy

function a() {
  let name = "John";
  
  function b() {
    console.log(name);  
  }
  
  return b;
}

const c = a(); // 'c' is now function 'b', which closes over 'name'

c(); // logs "John"

Did you notice what happened?

When we called the function a(), which is the parent function of function b, we assigned its return value to the variable c. After a() finished executing, it returned the inner function b. So now, c holds the function b. Since c is a function, we can call it using c(), and when we do, it prints "John".

Now, let’s understand what happened and why.

In the example, we created a variable called name inside function a, and accessed it within the inner function b. Then, function a returned function b. So when we called a(), it returned b, and a itself finished executing. We assigned the returned function b to the variable c, and later called c().

You might be wondering: What’s the point of all this?

The answer is: This is a closure.

Even though function a() had already finished executing and was removed from the call stack, the returned function b still had access to the name variable that was declared in a. That’s what defines a closure: a function that “remembers” its lexical scope even after the outer function has been completed.

So, function a returned a closure function b, which retained access to the variables from its outer scope.

Let’s dive deeper into a few tricky examples to understand this concept even better.

Take a look at the example below. It may seem a bit complex because we’ve created three functions, each nested inside the other. However, you’ll notice that the innermost function c can still access the variable x, which is declared in its grandparent function.

JavaScript

copy

function a() {
  let x = 1;
  function b() {
    let y = 2;
    function c() {
      console.log(x + y); // Has access to both x and y
    }
    return c;
  }
  return b();
}

const fn = a(); // returns function c
fn(); // logs 3

I wrote this tricky example to help clear up your confusion on the topic, you may have had this question in mind. That’s why this example is essential. But you might still be wondering: how does a nested function access a variable declared in its parent or even grandparent function?

When a function is defined, JavaScript doesn’t just store the function’s code — it also stores a reference to the scope (or lexical environment) in which it was created. This is what enables nested functions to access variables from their parent or even grandparent scopes.

Lexical refers to the location where code is written. For example, a function can access variables that are declared in the same scope or in any outer scope where it was defined. A function retains access to its outer scope even after the outer function has returned. This retained scope is called a closure.

Before we dive into why we need closures, I want to clarify one more point.

Take a look at the example below:

JavaScript

copy

function a() {
  let name = 'john';
  
  function b() {
    console.log("hello", name); // output: harry
  }
  
  name = 'harry';
  
  return b;
}

const functionB = a();

functionB();

Did you notice what the output is? Why is the output harry and not john? The answer is: when a parent function returns a nested function as a closure (meaning it returns a function along with its lexical environment), the reference to the variable is preserved, not its value at the time the closure was created.

For example, if the variable name is defined in the outer function, the closure captures a reference to name, not a copy of its value.

So, initially, when we declared the variable name, we assigned it the value john. But before returning the function b, we updated name to harry. Since closures retain a reference to the original variable, the returned function b accesses the updated value harry, not the initial value john.

Why do we need closures?

They are very useful in JavaScript, even though they are not widely used. However, understanding them is crucial for interviews. You might be asked various questions related to closures to test your skills. Some of the concepts I mentioned above are enough to help you tackle such questions. That said, there's one more thing I want to cover before wrapping up this topic.

Look at the example below. What output do you expect from this code snippet?

JavaScript

copy

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

Obviously, you would expect 0, 1, 2, 3, 4 to be printed every second, with each number printed as the loop runs. But the actual output is different, as you can see below:

JavaScript

copy

5
5
5
5
5

Did you notice what happened here? I know you might be a bit confused about why the output is different. It's because we used var inside the loop. If we had used let instead, the result would have been 0, 1, 2, 3, 4 as expected. You can see this in the example below:

JavaScript

copy

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

/* output:
0
1
2
3
4
*/

Have you noticed that the output is now correct, just as we expected, simply because we used let instead of var? But the question is, why does this happen?

Understanding this can be daunting but rewarding. In JavaScript, var is function-scoped and does not support block scope. When we use var in a for loop (which introduces a block scope, not a function scope), all iterations share the same i variable. Inside the loop, we use the setTimeout() function to display each number after a delay. However, the loop runs very quickly and finishes in less than a second — before any of the setTimeout callbacks execute. So when those delayed functions finally run, they all print the final value of i, which is 5, because that’s where the loop ended. This results in printing the number 5, five times, each after a one-second delay.

However, if we remove the setTimeout() function and let the loop run normally, it will output 0, 1, 2, 3, 4 instantly. Check out the example below:

JavaScript

copy

for (var i = 0; i < 5; i++) {
   console.log(i);
}

/* output 
0
1
2
3
4 */

So the final word about this is there:

var is function-scoped. When it is used in a for loop, it does not create a new scope for each iteration — the loop shares the same i variable across all iterations. That’s why setTimeout captures the final value of i. The previous values are not preserved because each iteration does not have its own separate i.

However, in the case of let, the loop also finishes in just a few milliseconds. Since let supports block scope, it creates a new private scope for each iteration. As a result, each setTimeout() callback captures its own copy of the i value. These callbacks then run one by one after their scheduled delays, producing the correct output.

Maybe you’re thinking about the example I showed you above, which doesn’t seem directly related to closures. But it actually is relevant because the function inside the setTimeout() is a closure. So an interviewer might test your understanding and try to trick you 😀. That’s why I didn’t want to skip this topic without explaining it.