JavaScript Closures Broke My Brain (Until They Didn't)

July 15, 2024

Okay, confession time. I avoided closures for months when I started learning JavaScript. Every tutorial made them sound like some mystical concept that only senior developers could grasp.

Then one day I was debugging a weird bug where all my buttons were doing the same thing. Turns out I'd been fighting closures without even knowing it. Once I understood what was happening, closures went from scary to... actually pretty useful?

The Moment It Clicked

I was building this simple image gallery. Had a bunch of thumbnails, each supposed to open a different modal. But every thumbnail opened the same modal - the last one. Super frustrating.

// This was my broken code
for (var i = 0; i < images.length; i++) {
  thumbnails[i].onclick = function() {
    openModal(images[i]); // Always the last image!
  };
}

Spent hours on this. Tried everything. Finally asked a coworker who just said "oh, closure problem" and fixed it in 30 seconds:

for (let i = 0; i < images.length; i++) {
  thumbnails[i].onclick = function() {
    openModal(images[i]); // Now it works!
  };
}

Changed var to let. That's it. But WHY did that work?

What's Actually Happening

Turns out closures aren't magic. They're just functions remembering stuff from where they were born.

With var, there's only one i variable that all the functions share. By the time you click anything, the loop finished and i is at its final value.

With let, each loop iteration gets its own i. So each function remembers its own copy.

It's like... imagine you're at a party and everyone gets a name tag. With var, everyone shares one name tag that keeps getting rewritten. With let, everyone gets their own name tag.

Where I Actually Use Closures

Once I got it, I started seeing closures everywhere. Not the textbook examples - real stuff I was already doing.

API calls with loading states:

function makeApiCall(url) {
  let isLoading = false;
  
  return async function() {
    if (isLoading) return; // Prevent double-clicks
    
    isLoading = true;
    try {
      const response = await fetch(url);
      return response.json();
    } finally {
      isLoading = false;
    }
  };
}

const getUserData = makeApiCall('/api/user');

Caching expensive calculations:

function expensiveCalculation() {
  let cache = {};
  
  return function(input) {
    if (cache[input]) {
      console.log('Cache hit!');
      return cache[input];
    }
    
    // Simulate expensive work
    const result = input * input * Math.random();
    cache[input] = result;
    return result;
  };
}

const calculate = expensiveCalculation();
calculate(5); // Does the work
calculate(5); // Returns cached result

The Counter Example (But Better)

Everyone shows the counter example. Here's why it's actually useful:

function createUniqueIdGenerator(prefix = 'id') {
  let counter = 0;
  
  return function() {
    return `${prefix}_${++counter}`;
  };
}

const generateUserId = createUniqueIdGenerator('user');
const generatePostId = createUniqueIdGenerator('post');

console.log(generateUserId()); // user_1
console.log(generatePostId()); // post_1
console.log(generateUserId()); // user_2

Each generator keeps its own counter. No global variables, no conflicts.

My Biggest Closure Mistake

I once created a memory leak that crashed our staging server. Had this code:

function setupEventListeners() {
  const massiveDataArray = loadHugeDataset(); // 50MB of data
  
  document.querySelectorAll('.button').forEach(button => {
    button.addEventListener('click', function() {
      // I only needed the button, but the closure
      // kept the entire massiveDataArray in memory!
      console.log('Button clicked');
    });
  });
}

Every time someone navigated to that page, we leaked 50MB. After a few hours, the server ran out of memory.

The fix was simple - don't reference variables you don't need:

function setupEventListeners() {
  const massiveDataArray = loadHugeDataset();
  processData(massiveDataArray); // Use it here
  
  // Don't reference massiveDataArray in the closure
  document.querySelectorAll('.button').forEach(button => {
    button.addEventListener('click', handleClick);
  });
}

function handleClick() {
  console.log('Button clicked');
}

React Hooks Are Just Closures

This blew my mind when I realized it. useState and useEffect are basically fancy closures:

// This is (simplified) how useState works
function useState(initialValue) {
  let state = initialValue;
  
  function setState(newValue) {
    state = newValue;
    // Trigger re-render
  }
  
  function getState() {
    return state;
  }
  
  return [getState, setState];
}

The state variable is "closed over" by both functions. That's how React components remember their state between renders.

When Things Go Wrong

Debugging closure issues is like trying to find your keys when you're already late. Frustrating and usually your fault.

My go-to move is just console.log everything. I know it's not sophisticated, but it works. I'll log variables at different points to see what the function actually has access to.

function problematicFunction() {
  console.log('What do I have access to?', { 
    someVar, 
    anotherVar, 
    thatThingFromOuterScope 
  });
}

Most of the time it's a loop issue. Like 90% of the time. You think each iteration gets its own copy of the variable, but nope - they're all sharing the same one.

The Chrome debugger is helpful too, but I always forget to use it until I've already spent an hour console.logging everything.

// Add this to see what's in the closure
function myFunction() {
  console.log('Closure variables:', {
    // List all the variables you think should be available
  });
}

Don't Be That Developer

Sometimes I see code that uses closures just because the developer learned about closures. Like this:

// Why would you do this?
function createAdder() {
  return function(a, b) {
    return a + b;
  };
}
const add = createAdder();

// When this exists
function add(a, b) {
  return a + b;
}

I've been guilty of this too. You learn a new concept and suddenly want to use it everywhere. But sometimes the simple solution is better.

Not everything needs to be clever. Sometimes a regular function is fine.

The Part That Still Confuses Me

There's this weird thing about closures that I understand intellectually but still catches me off guard:

function outer() {
  var x = 1;
  
  function inner() {
    console.log(x);
  }
  
  x = 2; // Wait, what happens now?
  
  return inner;
}

const fn = outer();
fn(); // Logs 2, not 1!

So the closure doesn't take a snapshot of x when it's created. It keeps a reference to the actual variable. Which means if x changes later, the closure sees the new value.

I know this. I understand this. But I still get surprised by it sometimes when debugging. My brain expects it to capture the value, not the variable itself.

JavaScript, man. Always keeping you on your toes.

What I Think Now (Maybe)

Honestly? Closures aren't that complicated once you stop trying to understand them academically.

I mean, you're probably using them already without realizing it. Every time you write onClick={() => doSomething()} in React, that's a closure. The arrow function "closes over" the doSomething variable from the outer scope.

But here's the thing that still trips me up sometimes - closures capture the variable itself, not the value. So if that variable changes later, the closure sees the new value. Which can be useful or confusing depending on what you're trying to do.

I don't think about closures much anymore when I'm coding. They just... happen. Like breathing. You don't think about breathing until someone mentions it and then suddenly you're very aware of your lungs.

JavaScript is still weird though. That hasn't changed.