Closures: Why Your Asynchronous Code “Works Sometimes”
Closure in JS

Closures: Why Your Asynchronous Code “Works Sometimes”

Following the theme of my previous articles about tools that operate without you noticing, here’s another one:

If you work with JavaScript, you use closures every single day, even without realizing it.

But unlike the other topics I’ve covered, closures are not an advanced feature. They’re simply how the language works.

Don’t worry, I’ll explain how it works and why understanding it more deeply helps you level up as a developer.


What Is a Closure?

A closure happens when a function keeps a reference to its lexical environment, regardless of when it is executed.

A simple example:

function contador() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

const incrementar = contador();
console.log(incrementar());
console.log(incrementar());
        

In this simple example, the values logged will be 1 and then 2.

Even after the contador function has finished executing, the returned function still has access to the count variable.

That is a closure.


Why Does This Happen?

Because in JavaScript, functions carry a reference to their Lexical Environment at the moment they are created.

That environment contains:

  • Variables;
  • Parameters;
  • Functions declared in that scope;

In other words, the function does not copy values. It keeps a reference to the variables.


A Real Production Bug Caused by Closures

Imagine an auto-save system that saves while the user is typing:

function criarAutoSave() {
  let draft = "";

  return function atualizarNovoTexto(novoTexto) {
    draft = novoTexto;

    setTimeout(() => {
      console.log("Saving:", draft);
      // enviarParaAPI(draft)
    }, 1000);
  };
}

const autoSave = criarAutoSave();

autoSave("First version");
autoSave("Second version");
autoSave("Third version");
        

What would you expect?

If you expect something like:

Saving: First version
Saving: Second version
Saving: Third version
        

I have bad news:

Saving: Third version
Saving: Third version
Saving: Third version
        

What happened here?

The setTimeout didn’t capture the value of draft at that moment. It captured the variable draft. And when the callback executes, the variable has already been updated multiple times.

Closures keep a reference to the variable, not a copy of its value.

This also connects to the Event Loop and microtask/macrotask management, topics I’ve written about before here and here, if anyone is interested. There is an English version in the end of those posts.


Where Does This Appear in the Real World?

This kind of bug is extremely common in:

  • Form auto-save systems;
  • Poorly implemented manual debounce;
  • Asynchronous processing inside loops;
  • Request queues;
  • Automatic retries;
  • Inconsistent logs;
  • Delayed notifications;

And the worst part:

  • The code looks correct;
  • There’s no error in the console;
  • It works “sometimes”;
  • It only breaks under fast user interaction;

It’s a classic production bug.


How Do You Fix It?

You need to capture the value at that moment:

function criarAutoSave() {
  let draft = "";

  return function atualizarNovoTexto(novoTexto) {
    draft = novoTexto;

    const snapshot = draft; // capture the value at call time

    setTimeout(() => {
      console.log("Saving:", snapshot);
      enviarParaAPI(snapshot);
    }, 1000);
  };
}

const autoSave = criarAutoSave();

autoSave("First version");
autoSave("Second version");
autoSave("Third version");
        

Now each timeout uses the correct value.

Even better: implement debounce properly and cancel previous timers. This was just an example scenario.


Closure Is Not the Problem

Closure is just the name given to a native JavaScript mechanism. It’s working exactly as designed.

The real issue is: Mutability + Asynchrony + Shared Reference.

When you understand that, you start recognizing patterns like:

  • Why stale state happens;
  • Why loops with var used to break;
  • Why Promises inside loops behave strangely;
  • Why callbacks use “old” values;


What Really Separates the Levels?

“The setTimeout is buggy.”, “The callback is using the wrong value.” or “This callback is closing over a mutable variable that will change before execution.”

What would your conclusion be when facing a problem like this?


Conclusion

Closures are not an optional feature in JavaScript, they are the foundation of how functions work in the language.

If you work with:

  • Asynchrony;
  • Events;
  • Promises;
  • Callbacks;
  • Timers;
  • React;
  • Node.js;

You are dealing with closures all the time.

And understanding them deeply completely changes the quality of your code.

To view or add a comment, sign in

More articles by Rafael Kepler

Others also viewed

Explore content categories