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:

  1. console.log(studentName);
  2. // ReferenceError
  3. 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:

  1. studentName = "Suzy"; // let's try to initialize it!
  2. // ReferenceError
  3. console.log(studentName);
  4. 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:

  1. let studentName = "Suzy";
  2. 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:

  1. // ..
  2. let studentName;
  3. // or:
  4. // let studentName = undefined;
  5. // ..
  6. studentName = "Suzy";
  7. console.log(studentName);
  8. // 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:

  1. askQuestion();
  2. // ReferenceError
  3. let studentName = "Suzy";
  4. function askQuestion() {
  5. console.log(`${ studentName }, do you know?`);
  6. }

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):

  1. var studentName = "Kyle";
  2. {
  3. console.log(studentName);
  4. // ???
  5. // ..
  6. let studentName = "Suzy";
  7. console.log(studentName);
  8. // Suzy
  9. }

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.