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:
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.
Recommended by LinkedIn
Where Does This Appear in the Real World?
This kind of bug is extremely common in:
And the worst part:
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:
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:
You are dealing with closures all the time.
And understanding them deeply completely changes the quality of your code.