I know Temporal Dead Zone sounds like a sci-fi phrase. But it's helpful to understand what the terms and concepts you work with everyday (or want to learn about) mean.

Strap in, because this gets complicated.

Are you aware that in JavaScript we can add { } to add a level of scope wherever we want?

So we could always do the below:

{ { { { { { var madness = true } } } } } }
Hopefully, you don't see this in production code!

I've included this detail to make sure that the upcoming examples makes sense (as I didn't want to assume that everyone knew it).

Before ES6 there was no other way to declare variables other than var. But ES6 brought us let and const.

let and const declarations are both block-scoped, which means they are only accessible within the { } surrounding them. var, on the other hand, doesn't have this restriction.

Here's an example:

let babyAge = 1;
let isBirthday = true;

if (isBirthday) {
	let babyAge = 2; 
}

console.log(babyAge); // Hmmmm. This prints 1
Two unique variables, with different values.

The above has occurred because the re-declaration of babyAge to 2 is only available inside the if block. Beyond that, the first babyAge is used. Can you see that they are two different variables?

In contrast, the var declaration has no block scope:

var babyAge = 1;
var isBirthday = true;

if (isBirthday) {
	var babyAge = 2; 
}

console.log(babyAge); // Ah! This prints 2
One variable with it's value re-declared. 

The final salient difference between let / const and var is that if you access var before it's declared, it is undefined. But if you do the same for let and const, they throw a ReferenceError.

console.log(varNumber); // undefined
console.log(letNumber); // Doesn't log, as it throws a ReferenceError letNumber is not defined

var varNumber = 1;
let letNumber = 1;

They throw the error all because of the Temporal Dead Zone.

Temporal Dead Zone explained

This is what the TDZ is: the term to describe the state where variables are un-reachable. They are in scope, but they aren't declared.

The let and const variables exist in the TDZ from the start of their enclosing scope until they are declared.

You could also say that the variables exist in the TDZ from the place they get bound (when the variable gets bound to the scope it's inside) until it is declared (when a name is reserved in memory for that variable).

{
 	// This is the temporal dead zone for the age variable!
	// This is the temporal dead zone for the age variable!
	// This is the temporal dead zone for the age variable!
 	// This is the temporal dead zone for the age variable!
	let age = 25; // Whew, we got there! No more TDZ
	console.log(age);
}
The temporal dead zone captured and catalogued.

You can see above that if I accessed the age variable earlier than its declaration, it would throw a ReferenceError. Because of the TDZ.

But var won't do that. var is just default initialized to undefined unlike the other declaration.

What's the difference between declaring and initialising?

Here is an example of declaring a variable and initialising a variable.

function scopeExample() {

    let age; // 1
    age = 20; // 2
    let hands = 2; // 3
}
Declaring vs Initialising a variable.

Declaring a variable means we reserve the name in memory at the current scope. That is labelled 1 in the comments.

Initialising a variable is setting the value of the variable. That is labelled 2 in the comments.

Or you could always do both on one line. That is labelled 3 in the comments.

Just to repeat myself again: the let and const variables exist in the TDZ from the start of their enclosing scope until they are declared.

So from the above code snippet, where is the TDZ for age? Also, does hands have a TDZ? If so, where is the start and end of the TDZ for hands?

Check your answer The hands and age variables both enter the TDZ.

The TDZ for hands ends when it gets declared, the same line it gets set to 2.

The TZ for age ends when it gets declared, and the name reserved in memory (in step 2, where I commented).

Why is the TDZ created when it is?

Let's go back to our first example:

{
    // This is the temporal dead zone for the age variable!
    // This is the temporal dead zone for the age variable!
    // This is the temporal dead zone for the age variable!
    // This is the temporal dead zone for the age variable!
    let age = 25; // Whew, we got there! No more TDZ
    console.log(age);
}

If we add a console.log inside the TDZ you will see this error:

Why does the TDZ exist between the top of the scope and the variable declaration? What's the specific reason for that?

It's because of hoisting.

The JS engine that is parsing and executing your code has 2 steps to do:

  1. Parsing of the code into an Abstract Syntax Tree/executable byte code, and
  2. Run time execution.

Step 1 is where hoisting happens, and this is done by the JS engine. It essentially will move all your variable declarations to the top of their scope. So an example would be:

console.log(hoistedVariable); // undefined
var hoistedVariable = 1;

To be clear, these variables aren't physically moving in the code. But, the result would be functionally identical to the below:

var hoistedVariable;

console.log(hoistedVariable); // undefined
counter = 1;

The only difference between const and let is that when they are hoisted, their values don't get defaulted to undefined.

Just to prove let and const also hoist, here's an example:

{
    // Both the below variables will be hoisted to the top of their scope!
	console.log(typeof nonsenseThatDoesntExist); // Prints undefined
	console.log(typeof name); // Throws an error, cannot access 'name' before initialization

	let name = "Kealan";
}

The above snippet is proof that let is clearly hoisted above where it was declared, as the engine alerts us to the fact. It knows name exists (it's declared), but we can't access it before it is initialized.

If it helps you to remember, think of it like this.

When variables get hoisted, var gets undefined initialized to its value by default in the process of hoisting. let and const also get hoisted, but don't get set to undefined when they get hoisted.

And that's the sole reason we have the TDZ. Which is why it happens with let and const but not var.

More examples of the TDZ

The TDZ can also be created for default function parameters. So something like this:

function createTDZ(a=b, b) {
}

createTDZ(undefined, 1); 

throws a ReferenceError, because the evaluation of variable a tries to access variable b before it has been parsed by the JS engine. The function arguments are all inside the TDZ until they are parsed.

Even something as simple as let tdzTest = tdzTest; would throw an error due to the TDZ. But var here would just create tdzTest and set it to undefined.

There's one more final and fairly advanced example from Erik Arvindson (who's involved in evolving and maintaining the ECMAScript spec):

let a = f(); // 1
const b = 2;
function f() { return b; } // 2, b is in the TDZ
One final TDZ example.

You can follow the commented numbers.

In the first line we call the f function, and then try to access the b variable (which throws a ReferenceError because b is in the TDZ).

Why do we have the TDZ?

Dr Alex Rauschmayer has an excellent post on why the TDZ exists, and the main reason is this:

It helps us catch errors.

To try and access a variable before it is declared is the wrong way round, and shouldn't be possible.

It also gives more expected and rational semantics for const (because const is hoisted, what happens if a programmer tries to use it before it is declared at runtime? What variable should it hold at the point when it gets hoisted?), and was the best approach decided by the ECMAScript spec team.

How to avoid the issues the TDZ causes

Relatively simply, always make sure you define your lets and consts at the top of your scope.