Uninitialized Variables (aka, TDZ)
With var
declarations, the variable is “hoisted” to the top of its scope. But it’s also automatically initialized to the undefined
value, so that the variable can be used throughout the entire scope.
However, let
and const
declarations are not quite the same in this respect.
Consider:
console.log(studentName);
// ReferenceError
let studentName = "Suzy";
The result of this program is that a ReferenceError
is thrown on the first line. Depending on your JS environment, the error message may say something like: “Cannot access studentName before initialization.”
NOTE: |
---|
The error message as seen here used to be much more vague or misleading. Thankfully, several of us in the community were successfully able to lobby for JS engines to improve this error message so it more accurately tells you what’s wrong! |
That error message is quite indicative of what’s wrong: studentName
exists on line 1, but it’s not been initialized, so it cannot be used yet. Let’s try this:
studentName = "Suzy"; // let's try to initialize it!
// ReferenceError
console.log(studentName);
let studentName;
Oops. We still get the ReferenceError
, but now on the first line where we’re trying to assign to (aka, initialize!) this so-called “uninitialized” variable studentName
. What’s the deal!?
The real question is, how do we initialize an uninitialized variable? For let
/const
, the only way to do so is with an assignment attached to a declaration statement. An assignment by itself is insufficient! Consider:
let studentName = "Suzy";
console.log(studentName); // Suzy
Here, we are initializing the studentName
(in this case, to "Suzy"
instead of undefined
) by way of the let
declaration statement form that’s coupled with an assignment.
Alternatively:
// ..
let studentName;
// or:
// let studentName = undefined;
// ..
studentName = "Suzy";
console.log(studentName);
// Suzy
NOTE: |
---|
That’s interesting! Recall from earlier, we said that var studentName; is not the same as var studentName = undefined; , but here with let , they behave the same. The difference comes down to the fact that var studentName automatically initializes at the top of the scope, where let studentName does not. |
Remember that we’ve asserted a few times so far that Compiler ends up removing any var
/let
/const
declarators, replacing them with the instructions at the top of each scope to register the appropriate identifiers.
So if we analyze what’s going on here, we see that an additional nuance is that Compiler is also adding an instruction in the middle of the program, at the point where the variable studentName
was declared, to handle that declaration’s auto-initialization. We cannot use the variable at any point prior to that initialization occuring. The same goes for const
as it does for let
.
The term coined by TC39 to refer to this period of time from the entering of a scope to where the auto-initialization of the variable occurs is: Temporal Dead Zone (TDZ).
The TDZ is the time window where a variable exists but is still uninitialized, and therefore cannot be accessed in any way. Only the execution of the instructions left by Compiler at the point of the original declaration can do that initialization. After that moment, the TDZ is done, and the variable is free to be used for the rest of the scope.
A var
also has technically has a TDZ, but it’s zero in length and thus unobservable to our programs! Only let
and const
have an observable TDZ.
By the way, “temporal” in TDZ does indeed refer to time not position in code. Consider:
askQuestion();
// ReferenceError
let studentName = "Suzy";
function askQuestion() {
console.log(`${ studentName }, do you know?`);
}
Even though positionally the console.log(..)
referencing studentName
comes after the let studentName
declaration, timing wise the askQuestion()
function is invoked before the let
statement is encountered, while studentName
is still in its TDZ! Hence the error.
There’s a common misconception that TDZ means let
and const
do not hoist. This is an inaccurate, or at least slightly misleading, claim. They definitely hoist.
The actual difference is that let
/const
declarations do not automatically initialize at the beginning of the scope, the way var
does. The debate then is if the auto-initialization is part of hoisting, or not? I think auto-registration of a variable at the top of the scope (i.e., what I call “hoisting”) and auto-initialization at the top of the scope (to undefined
) are distinct operations and shouldn’t be lumped together under the single term “hoisting.”
We’ve already seen that let
and const
don’t auto-initialize at the top of the scope. But let’s prove that let
and const
do hoist (auto-register at the top of the scope), courtesy of our friend shadowing (see “Shadowing” in Chapter 3):
var studentName = "Kyle";
{
console.log(studentName);
// ???
// ..
let studentName = "Suzy";
console.log(studentName);
// Suzy
}
What’s going to happen with the first console.log(..)
statement? If let studentName
didn’t hoist to the top of the scope, then the first console.log(..)
should print "Kyle"
, right? At that moment, it would seem, only the outer studentName
exists, so that’s the variable console.log(..)
should access and print.
But instead, the first console.log(..)
throws a TDZ error, because in fact, the inner scope’s studentName
was hoisted (auto-registered at the top of the scope). What didn’t happen (yet!) was the auto-initialization of that inner studentName
; it’s still uninitialized at that moment, hence the TDZ violation!
So to summarize, TDZ errors occur because let
/const
declarations do hoist their declarations to the top of their scopes, but unlike var
, they defer the auto-initialization of their variables until the moment in the code’s sequencing where the original declaration appeared. This window of time (hint: temporal), whatever its length, is the TDZ.
How can you avoid TDZ errors?
My advice: always put your let
and const
declarations at the top of any scope. Shrink the TDZ window to zero (or near zero) length, and then it’ll be moot.
But why is TDZ even a thing? Why didn’t TC39 dictate that let
/const
auto-initialize the way var
does? Just be patient, we’ll come back to explore the why of TDZ in Appendix A.