Sitemap

JavaScript Variable Scoping and Lifetimes

9 min readApr 21, 2025

--

Image Source

There’s a lot more structure behind variable behavior in JavaScript than it first appears. When you use var, let, or const, the engine decides where each variable goes in memory, how long it sticks around, and who can use it. These decisions come from a mix of scoping rules and automatic memory handling that runs in the background. This article will go over what actually happens when a variable is declared and later removed. We will walk through how scoping works for each type, how long variables live, and what the engine does under the hood to keep everything running.

Scoping Rules in JavaScript

How JavaScript Decides Variable Visibility

Every variable in JavaScript exists within a particular scope, which acts like a boundary that controls where the variable can be accessed from. This is not just a visual or structural boundary in the code. The JavaScript engine actively manages access to variables based on these scope rules. The type of scope a variable ends up in depends on how and where it’s declared, and this has a direct effect on how the code behaves when you try to use that variable elsewhere. When the engine starts running code, it creates something called an execution context. This is the internal setup that keeps track of everything happening at that point in time. It includes variables, functions, and even the value of this. These contexts stack on top of each other as functions call other functions, creating layers of environments. Each one keeps track of its own variables.

JavaScript uses lexical scoping, which means the structure of the code, meaning where things are written, determines the scope. It doesn’t matter when a function is called or how it’s executed. The engine always looks at where it was originally written to figure out what variables it can see.

There are three main types of scope:

  • Global scope: The outermost context. Anything declared here is available across the entire program.
  • Function scope: Created when a function runs. Variables declared with var inside a function are stuck inside that function and can’t be seen from the outside.
  • Block scope: Created with {} braces, such as in if statements, for loops, and so on. This is where let and const behave differently from var.

What Makes var Act the Way It Does

The var keyword has been part of JavaScript from the beginning, and it behaves in ways that don’t always match what developers expect. It doesn't respect block boundaries. If you declare a variable with var inside an if statement or loop, it doesn’t stay locked inside that block. It leaks out into the entire function.

function testVar() {
if (true) {
var message = 'Visible outside the block';
}
console.log(message); // Works fine
}

Even though message was declared inside the if block, it’s available in the whole function. That’s because var only pays attention to function boundaries. This kind of behavior can lead to mistakes, especially in larger functions where it’s easy to lose track of where a variable was declared.

On top of that, var declarations get hoisted. That means the engine sets up the variable at the top of its scope before any code runs, not with the value you assigned, but just with a placeholder value of undefined.

function hoistExample() {
console.log(a); // undefined, not an error
var a = 5;
}

The code acts as if var a was moved to the top of the function. This can make debugging tricky because the code looks like it's accessing a variable before it exists. But technically it does exist; it just hasn’t been given its real value yet.

Why let and const Behave Differently

With the introduction of let and const in newer versions of JavaScript, variables finally started behaving more like developers expected. These two keywords respect block boundaries. If you declare a variable inside {}, it’s only visible inside those braces.

function testLet() {
if (true) {
let greeting = 'Hi';
const name = 'Code';
console.log(greeting, name);
}
// greeting and name are not accessible here
}

Trying to use greeting or name outside of that block causes a reference error, because the variables are truly limited to the block where they were defined.

With var, variables are initialized when the code is first loaded. But let and const are not — the engine sets them up internally but doesn't allow them to be accessed until the exact line of code where they appear. The space between the start of the block and the variable declaration is known as the temporal dead zone.

function temporalDeadZoneExample() {
console.log(count); // ReferenceError
let count = 3;
}

Here, the variable exists, but it can’t be used yet. The engine throws a reference error if you try. This behavior helps catch bugs early, because it prevents access to variables that aren’t ready to be used.

When and Why Scoping Decisions Matter

Variable scoping affects everything from loops to conditionals to callbacks and closures. If you’ve ever used var in a loop and seen odd behavior in a callback, it’s often because of the way scoping was handled.

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // Logs 3, 3, 3
}

for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // Logs 0, 1, 2
}

With var, the same i is used in every callback. By the time the timeout runs, the loop has already finished and i is 3. With let, each iteration of the loop creates a new block-scoped j, so the value at each step is preserved in the callback.

Get Alexander Obregon’s stories in your inbox

Join Medium for free to get updates from this writer.

These small differences in how scoping works can have a big impact on how your code behaves. Knowing how the JavaScript engine treats each variable type helps you avoid bugs that are otherwise hard to track down.

Variable Lifetimes and Memory Mechanics

What Happens After a Variable Is Declared

Once the JavaScript engine has created a variable, it doesn’t just forget about it. There are internal systems that track how long a variable sticks around, how it’s stored, and when it can be safely removed. These mechanics are built into the engine and apply whether you’re writing a small script or a large application. Even though this work is done for you, knowing how it functions in the background helps make more sense of some behavior that might otherwise seem odd.

A variable’s lifetime refers to how long its value stays in memory before the engine discards it. This can be as short as a few milliseconds or as long as the entire runtime of a program. The timing depends on what kind of variable it is, how it’s used, and whether anything is still referencing it.

Variables declared inside functions, for example, are usually temporary. They’re created when the function starts, and once the function finishes and nothing else points to those values, the engine is free to throw them away. But there are exceptions, such as when inner functions still need access to those variables.

How the Engine Sets Things Up Internally

When JavaScript begins running a function, it moves through two phases: one to prepare everything, and one to actually run the code. These are often referred to as the setup and execution steps. During the setup step, the engine figures out what variables exist, what kind of declarations they use, and where they belong. During the execution step, the engine starts evaluating expressions, calling functions, and assigning values.

Variables declared with var are assigned memory space early on, right at the setup stage. Even if you use them later in the function, the engine already made room for them at the top of the scope. For let and const, the engine knows they exist, but doesn't assign space or let you access them until the line where they’re written is reached. That’s why you get a reference error instead of undefined if you try to use them too early. At a lower level, each execution context keeps a kind of internal record where it stores variable bindings. This record is used to track which variables belong to that context and what values they currently hold. Once the context finishes and the engine no longer needs it, that record can be thrown away, but only if nothing else depends on it.

Why Some Variables Stick Around Longer

In some cases, variables outlive the block or function they were created in. This happens when another part of the code still refers to them. A common example is when a function returns another function that uses variables from its outer function. This is called a closure, and it’s one way variables can hang around far longer than expected.

function counter() {
let count = 0;
return function () {
count++;
return count;
};
}

const getCount = counter();
getCount(); // 1
getCount(); // 2

Even though counter() has finished running, the variable count is still alive. That’s because getCount continues to use it. The JavaScript engine keeps count stored in memory and prevents it from being cleaned up, because it’s still tied to an active function.

The engine moves such values to the heap, which is an area of memory designed for long-term storage. This is different from the stack, which is where short-lived values like local variables usually go. Once no more references exist to a heap value, it becomes eligible for removal.

How Cleanup Works in the Background

JavaScript uses automatic garbage collection to manage memory cleanup. This means you don’t have to manually delete variables when you’re done with them. The engine monitors memory usage and removes values that are no longer needed.

The basic rule is: if a value is no longer accessible by any part of the code, it’s considered unused and can be removed. This includes variables, objects, arrays, and even functions. The engine runs periodic checks to find out what can be cleared and what still needs to stay.

function temporaryData() {
let obj = { temp: 'data' };
// The object is only used inside this function
return 'done';
}

In the example above, once temporaryData() finishes and no outside code holds a reference to the obj, the object becomes unreachable. The engine will eventually clean it up, freeing that memory space for future use.

Garbage collection usually happens in cycles. The engine builds a map of what’s reachable and what’s not. If something isn’t connected to any part of the active code, it gets marked as garbage. The engine then reclaims that space. You don’t have control over exactly when this happens, but you can avoid keeping data in memory longer than necessary by not holding global references or long-lived timeouts to unused data.

Closures and Memory Retention

While closures are useful for things like preserving state or creating private variables, they also affect memory. Any variable captured by a closure is kept in memory for as long as the closure is accessible.

function outer() {
let bigData = new Array(1000000).fill('x');
return function inner() {
console.log(bigData[0]);
};
}

const hold = outer();
// bigData stays in memory because inner still needs it

For this, bigData will not be released until hold is no longer used. If you're building something like a long-running process, such as an event handler or a repeating interval, it’s important to keep track of what variables might be sticking around because of closures.

This doesn’t mean closures are bad. It just means they cause the engine to hold onto values that would otherwise be cleared. In many cases, this is exactly what you want. But if you’re creating a lot of short-lived closures that reference large datasets, memory use can grow more than expected.

Conclusion

The way JavaScript handles variable scoping and lifetimes isn’t random. The engine follows a series of steps to decide where each variable belongs, how long it should stay in memory, and when it’s safe to let go of it. From function calls to block-level decisions to delayed cleanup with closures, the entire system runs with rules that are consistent and traceable. Once you understand how these mechanics fit together, the behavior you see in code starts to make more sense.

  1. JavaScript Scoping on MDN
  2. Execution Context on MDN
  3. JavaScript Closures on MDN
  4. JavaScript Memory Management on MDN

Thank you for reading! If you find this article helpful, please consider highlighting, clapping, or responding. I also share weekly recaps and extra content on Substack, and subscribing there helps me keep my content here free for everyone to read.

--

--

Alexander Obregon
Alexander Obregon

I post daily about programming topics and share what I learn as I go. For recaps, exclusive content, and to support me: https://alexanderobregon.substack.com