Skip to content

Closure in JavaScript, or how a function can remember things

Profile photo of Adrian Zawadzki

Adrian Zawadzki

14 min read

Functions in JS usually disappear together with their local memory. Closure breaks that rule - and it is the foundation for the module pattern, memoization, asynchronous JS, and private state without classes.

The intuition most people have at the start is this: a function is short-lived. It enters, creates its local memory, runs the code, returns a result, and disappears together with everything it had inside. On the next call it starts from scratch.

That intuition is good enough for most everyday code. It stops being enough when you see something like this:

function gradient() {
  let step = 0;
  return () => `hsl(${step++ * 30}, 72%, 56%)`;
}

const next = gradient();

Click - each call returns the next color in the sequence. gradient() finished right after next was created, and yet step lives somewhere and grows with every call:

next() returnedthe value of this single call
-
not called yet
closure remembersthe only thing actually captured
step=0

one number, living in the captured scope.

UI logthe UI keeps past returns visible - the closure has no access to this
empty

Notice the split in the component. At the top you see two pieces of information tied to a single call: the color the function just returned and step = N - the number next keeps between calls. That’s the state on the function side. Below you have a strip with previous colors - that’s a log kept by the UI, not by the function. If you asked next “what colors did you return earlier?” - it could not answer, because it does not know. It only knows how many times it was already called.

If your intuition says local memory disappears together with the function, it’s incomplete - because step lives exactly there, in the place that should already have been cleaned up.

#A function does not return only its code

When gradient() returns the inner function, it also returns a reference to the surroundings (the scope) where that function was defined. step is not copied - the returned function keeps a live reference to it. When invoked, JS first looks up variables in its local memory, then in this attached environment, and only at the end in the global one.

Formally, every function has a hidden property pointing to the scope in which it was created - in the older spec called [[Scope]], in the current one [[Environment]]. That reference is set once, at function creation time, and stays with the function for the rest of its life - keeping alive every variable from the surroundings that the function refers to.

What does it point to exactly? To a structure called the Environment Record in the spec. That’s a place in the engine’s memory holding a list of variables and their values. Every Environment Record has an [[OuterEnv]] field pointing to its parent - and that’s the chain a variable lookup walks until it finds the variable or hits the global environment. The same structure shows up in the post on execution context - that one shows how it’s built fresh for every function call on the call stack.

Picture it like this: a backpack attached to the function. The function leaves its parent carrying a backpack with everything its code refers to. The backpack stays with it as long as the function itself lives.

#Where it was defined, not where it was called

A non-obvious takeaway from this picture: the backpack is packed already when you write the code - not at runtime. Where you define the function, that’s the data it gets. The place where you call it has nothing to say here.

This is lexical (i.e. static) scoping. You look at the code, see where function sits, and you already know what it has access to. If JS worked the other way - with dynamic scope - closure in this form would not exist, because the function would have no way of knowing what to pack.

#Every call gets its own backpack

Another consequence of the same mechanism - every call to gradient() creates a new, separate step instance and a new function with its own remembered surroundings.

You can see this from two angles below. In the Two calls to gradient() tab we have two independent calls - each with its own surroundings, which were returned but still live (hence the dashed border). Click the button to see how variable lookup works: the function looks for step in its local memory, doesn’t find it, and walks up the chain of environments - until it hits the one stored in the backpack. In the Aliasing tab the same function is assigned to another variable - both names refer to one environment.

Each gradient() call produces its own captured scope - with its own step.

const a = gradient()first gradient() call
↑ captured scopeouter · survives return
step:0
↓ function body (this call)idle
step++; return step;
const b = gradient()second gradient() call
↑ captured scopeouter · survives return
step:0
↓ function body (this call)idle
step++; return step;

The key takeaway: the place where step lives is baked into the function’s definition through the hidden [[Environment]] property. You cannot create a new backpack by giving the function a new name - the backpack only comes into existence together with a new call to gradient().

In the spec this step is called NewFunctionEnvironment. When you call a function, the engine creates a new Environment Record and links its [[OuterEnv]] to whatever sits in the function’s [[Environment]]. Only inside that new environment does the body run. Every call to gradient() goes through the same step from scratch - that’s why you get a separate Environment Record, a separate step, a separate “instance” of the closure.

#What sits in the backpack is a reference, not a copy

It’s worth saying clearly what exactly sits in the backpack. It is not a snapshot of values taken at the moment the function was created - it’s a live reference to the variable. If the variable’s value changes, the function will see the new one when called.

The most famous consequence of this behavior is the classic for-loop with var. Three functions, three backpacks - but each one holds the same variable i, because var has function scope and all iterations share one place in memory. let has block scope, so every iteration gets its own i, and every callback closes over a separate one.

Step through it below - you can see the callbacks coming in, the binding growing (var) or multiplying (let), and finally how every callback reads its binding at fire time (not at creation time).

Step 0 / 7Start: loop hasn't run yet.
varone shared binding
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
callbacks queued:
cb0-
cb1-
cb2-
bindings in memory:
no binding yet
output:
···
letthree separate bindings
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
callbacks queued:
cb0-
cb1-
cb2-
bindings in memory:
-
-
-
output:
···

In the spec this rule has a name: CreatePerIterationEnvironment. For for/for-in/for-of loops with let or const the engine creates a new Environment Record on every iteration and copies the current value of the loop variable into it. Every callback defined in the loop body closes over that one specific Environment Record - not over a shared i. For var this operation simply does not exist: there is one shared Environment Record for the whole loop.

Before let, the only way to get this effect was to manually wrap the loop body in an IIFE to force a new scope per iteration.

#What else it’s called

With the backpack model in hand it’s easier to navigate the names that float around the same phenomenon. It pays to tell apart which come from formal documentation and which from teaching materials - they carry different weight, and you’ll meet both.

In the spec and documentation:

  • Environment Record - the formal name from ECMAScript. That’s the “backpack” in the language of the spec.
  • Closure - the most general term for the concept. The backpack is a concrete consequence of JS having lexical scope; closure itself exists in many languages, in different flavors.

In teaching materials (mainly Will Sentance’s courses):

  • C.O.V.E. - Closed Over Variable Environment. A mnemonic popularized by “JavaScript: The Hard Parts” on Frontend Masters - you won’t find it in ECMAScript or MDN. Same thing as Environment Record, different label.
  • P.L.S.R.D. - Persistent Lexical Static-scoped Referenced Data: Persistent (the data persists), L/S (lexical and static scoping), Reference (attached via [[Environment]]), Data (the actual content). Also from Sentance, unseen outside his ecosystem.
  • Variable environment - a casual term for the local memory of a call.

It’s all the same mechanism under different labels - it’s worth knowing both sides, because in English you’ll write code review using Environment Record, while in a chat with a junior you’ll hear “it’s the backpack from COVE”.

#What it gives you in practice

Closure is not a language curiosity - it’s the mechanism a surprisingly large slice of everyday code rests on:

  • Private state without classes. A counter, a cache, a feature flag - whatever you want to hide from the world.
  • Memoization. The first call computes, the result lands in the backpack, subsequent calls just read it.
  • Module pattern. App state in one place, no global pollution.
  • Async JS. Every callback and every continuation after await is in practice a function that remembers where it was when it went to sleep.

Once you see these four things as different uses of the same mechanism, most asynchronous and “magical” JS stops looking magical.

#The price you pay

The backpack holds a reference, so it also holds the data alive. As long as the function lives, its backpack lives with everything that fell into it.

Newer engines (V8) are smart and pack only the variables the function actually refers to - it’s no longer “close everything just in case”. Historically it was worse: older engines (V8 included, roughly until 2013) closed over the whole outer scope regardless of whether the function used all the variables. The classic example is the memory leak discovered in Meteor, where two nested functions shared one lexical environment and one of them kept alive a huge object it didn’t actually use.

Modern engines pack more sensibly, but the foundation is the same: if you close a large data structure into a closure that the function refers to, and you keep that function around permanently somewhere - that data will stay in memory whether you use it or not. Closure is not a free lunch.

The reachability mechanism, and why closures are one of the four classic sources of memory leaks in JS, is covered separately in the garbage collection post - there’s also an interactive “listener holding DOM” pattern in the mark-and-sweep widget there.

#Punchline

Everything boils down to one sentence: a function has access to what it saw when it was created - and carries that with it as long as it lives.

The rest - private state, memoization, modules, async - are different uses of the same property. Once that mechanism clicks in your head, code built on it stops looking like a stack of separate tricks and starts looking like one consistent pattern in different costumes.

Comments

Loading comments…

Leave a comment