Written: January 5, 2020

Edited: February 11, 2023

Closures can feel really tough, but they're extremely useful and relatively straightforward once they "click."

I hope this article helps you understand as much as it did when I wrote it.

Explaining closures is a frequently-used JavaScript and TypeScript interview question, but many engineers still don’t fully understand how they work. There’s a fog that surrounds it, much like the feeling you get when you eat too much during the holidays. 🥧

When I first started learning closures, my first impulse was to close my laptop screen and go stare existentially at the local pond. But don’t worry; closures trip up a lot of people, but you’ll understand how they work and the concepts behind them by the end of this article.

I’m going to walk you through a really cool (and more importantly, simple) example. You'll get it in no time at all.

Grab your favorite coffee, apple cider, hot chocolate, or wintery beverage, and let’s get started.

"I'll use any excuse to buy a new backpack."

- Michael Potts

Closures are an interesting concept. They are truly invisible in your code, and yet they are very real. If you've written any JavaScript or TypeScript, you've almost certainly used them without even realizing it.

If nothing else, that should give you comfort; if you're already accidentally using them successfully, half the battle will just be learning the ideas behind them to fill in the gaps of how they work.

At a very base level, a closure is an invisible compartmentalization of variables that are accessible to your code at various levels (e.g. the global scope, local function scope, etc.).

Each time you write a function, a closure is created for the scope available to it. Our compiler does this automatically, so all we need to do is understand it.

For instance, let's say we have the following code:

const globalVariable = 1;

function createClosure() {
  const localVariable = 2;

  return function () {
    return globalVariable + localVariable;
  };
}

const closure = createClosure();

console.log(closure()); // returns 3

This code will log the number 3 to the console. Multiple scopes have been created for your code behind the scenes; there is a global scope with the globalVariable in it and local scopes with the localVariable and globalVariable accessible in them.

Here's the same code with comments included to indicate which parts are global and local scopes:

// global scope with globalVariable available
const globalVariable = 1;

function createClosure() {
  // local scope with localVariable and globalVariable available
  const localVariable = 2;

  return function () {
    // local scope with localVariable and globalVariable available
    return globalVariable + localVariable;
  };
}

const closure = createClosure();

console.log(closure()); // returns 3

Stop and think about what's happening here for a moment; the createClosure function can access both variables.

What if I told you that this ability of closures to access all variables in scope at time of the function's creation could be used later when the function is used elsewhere, even another file?

I know. Has science gone too far?

Let's find out. 🧪

“A good traveler has no fixed plans and is not intent on arriving."

- Lao Tzu

Here's the code we're going to be working through for the rest of the article. Go ahead and read through it. 👍🏻

Don't worry if you don't understand how it relates to closures yet. Just get an idea of what code is actually doing:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") {
    return null;
  }

  let num = startingNum;

  function increment() {
    num++;
    return num;
  }

  function decrement() {
    num--;
    return num;
  }

  function getNum() {
    return num;
  }

  return [increment, decrement, getNum];
}

When we invoke that function, here's what we get out of it:

const [increment, decrement, getNum] = createCountingClosure(0);

Furthermore, we can invoke those increment, decrement, and getNum functions to get the following:

increment(); // 1
increment(); // 2
increment(); // 3
decrement(); // 2
increment(); // 3
getNum(); // 3

Whoa.

Don't worry. We're about to work through this code in detail to figure out exactly what's going on.

(Side note - We're doing some destructuring here. If you don't know what destructuring is, no worries. Check out MDN's excellent article on it.)

Let's look at that createCountingClosure function again and break down each part:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") {
    return null;
  }

  let num = startingNum;

  function increment() {
    num++;
    return num;
  }

  function decrement() {
    num--;
    return num;
  }

  function getNum() {
    return num;
  }

  return [increment, decrement, getNum];
}

First, we're taking in an argument called startingNum. We are clearly expecting it to be a number (as the next line, an if statement, returns null if it isn't).

Next, we're assigning the value of startingNum to the num variable. This gives our counter function a starting value to work with and change later.

We then proceed to declare three functions:

  1. increment, which increases the value of num and returns its value
  2. decrement, which decreases the value of num and returns its value
  3. getNum, which returns the current value of num
  4. Finally, we return these three functions inside an array. As functions are "first class citizens" in JavaScript and TypeScript, we can pass functions around like this to be returned from our function and used elsewhere.

This is why, later on, we can invoke createCountingClosure and destructure out these three functions:

const [increment, decrement, getNum] = createCountingClosure(0);

As you can see, we're also passing 0 into our function. Remember how we set it up to accept a startingNum argument? That value is going to be 0 in this example.

With all of that out of the way, we're now at the part of discussing the createCountingClosure function where we can talk about how the closures work.

“Not all those who wander are lost.”

- J.R.R. Tolkien

Let's take a look at the createCountingClosure function again:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") {
    return null;
  }

  let num = startingNum;

  function increment() {
    num++;
    return num;
  }

  function decrement() {
    num--;
    return num;
  }

  function getNum() {
    return num;
  }

  return [increment, decrement, getNum];
}

When we declared increment, decrement, and getNum, we unknowingly created little closure "backpacks" for each of them that store access to the all variables available in their scope when these functions were created.

Remember at the start of this article how we had global and local scope? Remember how the function we created had a local scope that retained access to the globalVariable in the global scope?

Well, the same thing is happening here. For instances, the increment function retains access to the num variable. In fact, all of these functions can still access this variable later on anywhere that we might use them (even if it's in a different file).

For example, remember how we destructured out all of these functions when we invoked createCountingClosure?

const [increment, decrement, getNum] = createCountingClosure(0);

Here's what the increment function has access to (in code) at the time it's destructured out as we did above:

let num = startingNum;

function increment() {
  num++;
  return num;
}

Even though we've only destructured out increment, it still has access to the num variable. It's in the function's closure backpack, along for the ride like a tiny little Yoda.

What this means is that we can access and modify our num variable even though we don't have it directly inside the function itself.

Isn't that wild? 🦁

This same thing is true for decrement and getNum, too. They both have access to num in the same manner through their closure from when they were created.

Let's go ahead and use our code to see how this works.

"I would gladly live out of a backpack if it meant I could see the world."

- Unknown

We're going to use our code and walk through it step by step to observe what it does. Here's a refresh of the entire thing before we get started:

function createCountingClosure(startingNum) {
  if (typeof startingNum !== "number") {
    return null;
  }

  let num = startingNum;

  function increment() {
    num++;
    return num;
  }

  function decrement() {
    num--;
    return num;
  }

  function getNum() {
    return num;
  }

  return [increment, decrement, getNum];
}

In addition, we're going to invoke our createCountingClosures function and destructure out the functions returned in the array like we did previously:

const [increment, decrement, getNum] = createCountingClosure(0);

With this setup, we're now ready to go. If you want to play around with this while we walk through the rest of this article, here's a codepen link that will allow you to interact with the code.

First off, we're going to invoke the increment function:

increment(); // should return 1

As we've previously discussed, the increment function retains access to the num variable. When we invoke increment, the function will increase the value of num behind the scenes.

Since we passed in the value of 0 when we initially invoked createCountingClosures up above, we increment that value to 1.

(If you're following along in the CodePen I provided, you'll see that pop up in the console because I've wrapped the function call in a console.log.)

Next, we'll invoke increment two more times like so:

increment(); // should return 2
increment(); // should return 3

Notice that the value being returned each time is not starting at 1; this is because the same variable num that we used above is also retained by increment and modified each time it is invoked. This is the incredible power of closures!

Next, we're going to call the function decrement to decrease the value of num:

decrement(); // should return 2

Notice that the value is, once again, not starting at 0. The functions decrement and increment both retain access to the same num variable and share the ability to modify it even though it is not contained inside either of them.

Finally, we're going to call increment one more time and cap it off by calling the getNum function (which just returns the current integer value of num):

increment(); // should return 3
getNum(); // 3

All of these function calls have access to the num variable as all of them have the reference to it stored away in their little scope "backpack."

The final getNum call returns 3, which is the value of num from all of the many changes we've made to it. Even though num is not contained directly inside any of the functions we've been invoking, we've modified it through the power of closures.

I hope that this brief overlook of how closures work helps you grasp them.

By imagining them as little backpacks that functions can wear as they move around your application, you can build a framework in your mind for how you can access and manipulate variables contained inside of closures.

Also, now that we've stepped through how they work, you can be confident in your closure knowledge and crush your next job interview.

Thanks for reading.

Nathan