Variable shadowing, hoisting and the temporal dead zone

Alex Bostock
5 min readMay 16, 2021

I wrote some JavaScript that didn’t work. It looked something like this:

var x = 1;
function f() {
let x = x;
// Something that modifies x
};
f();

Executing that results in an error. But why?

Variable shadowing

I assume you are at least somewhat familiar with how local and global variables behave: a local variable cannot be accessed from outside its scope. Consider this example:

let x = 3;
if(x === 3) {
let y = 4;
console.log(x); // 3
console.log(y); // 4
}
console.log(x); // 3
console.log(y); // ReferenceError

Because y is a local variable, scoped to the if block, it cannot be accessed from outside that block. But x is a global variable, so it can be accessed from anywhere.

But what if we have two variables of the same name?

let x = 3;
if(x === 3) {
let x = 4;
console.log(x); // 4
}
console.log(x); // 3

This is called variable shadowing: there are two separate variables with the same name, in separate (but nested) scopes.

Let’s note two key observations:

  • Variable shadowing is completely valid JavaScript.
  • When within the scope of the local x (the if block), there is no way to access the global x.

Hoisting

The next thing to know about is hoisting. Every variable in JavaScript is defined at the beginning of its scope (a variable’s scope is usually either the function in which it is defined, or the nearest pair of braces containing it). This may not be the same place where it is defined in the code. Consider this example:

function f() {
console.log(x); // undefined
var x = 3;
console.log(x); // 3
}

The first console.log refers to x, which is not defined until the line after. Because of hoisting, that function is equivalent to this one:

function f() {
var x;
console.log(x); // undefined
x = 3;
console.log(x); // 3
}

At the beginning of the scope (the function f), x is declared as a variable, but it initially has the value undefined. On the next line, we can access x without any error.

Key observations:

  • A variable may be defined before its first appearance in the code, possibly catching you by surprise.
  • Even if you assign a variable at the same time it is declared, accessing that variable still may return undefined.

The temporal dead zone

As we saw in the previous section, a hoisted var variable initially has the value undefined. When a let / const variable is hoisted, it doesn’t have a value: it is uninitialised.

If we try to access a let / const variable after it has been declared (at the point where it was hoisted to) and before it has been assigned a value, we get an error. This time between the variable being declared and the first time it is assigned a value is called the temporal dead zone (the TDZ).

Consider this example:

function f() {
console.log(x); // RefenceError
let x = 3;
console.log(x);
}

Due to hoisting, this is equivalent to:

function f() {
let x;
// TDZ begins here
console.log(x); // ReferenceError
// TDZ ends here
x = 3;
console.log(x);
}

Key observation about the TDZ:

  • If we attempt to access a let / const variable after it is defined but before it is assigned a value, we get an error.
  • This differs from var variables, which would return undefined in the same situation.

What’s wrong with my code?

Here’s my code again. Why do we get an error?

var x = 1;
function f() {
let x = x;
// Something that modifies x
};
f();

Considering hoisting, it is equivalent to this:

var x = 1;
function f() {
let x;
x = x;
// Something that modifies x
};
f();

The error appears at x = x. What does the x on the right hand side refer to? Because this is after let x, we now have a local x shadowing the global x. This means it can’t refer to the global x. So it must refer to the local x.

But the local x is in the TDZ, so accessing it is an error. And there’s the answer: there is an error because the variable I wanted to access is shadowed by a hoisted variable which is in its TDZ.

A Node.js session showing “ReferenceError: Cannot access ‘x’ before initialization” when my code is executed

What about var?

Due to lack of understanding, one of my first attempts at fixing this was swapping let for var:

var x = 1;
function f() {
var x = x;
// Something that modifies x
};
f();

What happens now?

The key difference between let and var is that var variables don’t have a TDZ: they just return undefined. Variable shadowing and hoisting are exactly the same as before.

So we don’t get an error, but the local x is assigned the value undefined (the initial value of the local x).

So switching let to var doesn’t make the program work. But rather than throwing an error, it leaves an undefined value for some other line of code to deal with.

How can fix this?

Use better variable names. Suppose we rename the local variable:

var x = 1;
function f() {
let xCopy = x;
// Something that modifies xCopy
};
f();

Now there is no variable shadowing, and the code works as expected.

The only problem left is that naming things is hard, but I’m not going to get into that here.

Scope

Talking about scope, variable shadowing, hoisting and TDZs may seem a little removed from a typical experience of writing JavaScript. I’ve spent a lot of time writing JavaScript, and I went a long time without learning about hoisting.

But sometimes it’s worth understanding a little more about how things work. A little knowledge can be very useful in finding bugs, or even preventing bugs from appearing.

There’s a lot more to know about scope in JavaScript:

  • What is a closure?
  • Exactly where does one scope end and another begin?
  • Exactly what is the difference between let, const and var?

I’d recommend You Don’t Know JS for an explanation of all of these topics.

--

--