JavaScript ES 6: let and the dreaded Temporal Dead Zone

Sam Bakkila
4 min readJul 6, 2017

--

I’ve seen a lot of misunderstanding between var and let in JavaScript.

The differences at first seem deceptively simple:

  • Declaring variables with let, which is block scoped (scoped to any use of curly brackets) rather than function scoped (like var)
  • This solves a lot of the issues that developers who are used to other languages have with Javascript — like the pollution of the global namespace from variables declared outside of functions.

However, there are some performance related issues with let that not enough people know about, particularly related to the ghastly sounding “Temporal Dead Zone”. Because of this, I recommend only using let if you have a particular need for let — more on this later.

What is the temporal dead zone?

I’ve seen some misinformation about this all around the internet. A lot of the stackoverflow answers on this topic are either wrong or at least a little misleading. Definitely refer to the most trusted sources on ES 6 that you can find for unusual JavaScript language features.

Here is the MDN’s explanation of the temporal dead zone:

In ECMAScript 2015, let bindings are not subject to Variable Hoisting, which means that let declarations do not move to the top of the current execution context. Referencing the variable in the block before the initialization results in a ReferenceError (contrary to a variable declared with var, which will just have the undefined value). The variable is in a “temporal dead zone” from the start of the block until the initialization is processed.

By itself, this doesn’t seem to be a problem. Many developers would prefer to get an error when trying to use a variable that hasn’t been initialized yet rather than have the code execute with the variable having the value undefined (which is what happens with var hoisting). I agree with this; I would much rather get an error message than have my code fail silently.

But the Temporal Dead Zone is not your friend…

However, there are some performance issues that arise from this temporal dead zone. There’s actually an open issue about this in the Node.js github repository.

You can play around with this snippet of code to see these performance problems firsthand:

console.time(‘var’);
for (var i = 0; i < 100000000; i++) {}
console.timeEnd(‘var’); // about 50 ms on my machine

console.time(‘let’);
for (let j = 0; j < 100000000; j++) {}
console.timeEnd(‘let’); // about 200 ms on my machine

There’s a lot of quirks to this that are best understood by reading the issue on the github page, but the take home points are:

  1. let typically performs more slowly than var
  2. some of the slower performance is to be expected and is connected to the core functionality of let (more execution contexts! sometimes you need this!)
  3. having variables in the temporal dead zone can drastically slow down seemingly unrelated code in the same scope. Try the example below if you want proof:

console.time(‘var’);
for (var i = 0; i < 100000000; i++) {}
console.timeEnd(‘var’); // now much slower, around 300 ms

console.time(‘let’);
let j // this declaration places j in the same scope as i, spreading the TDZ //performance problems
for (j = 0; j < 100000000; j++) {}
console.timeEnd(‘let’); // still 300+ ms

My recommendations for dealing with this are:

  1. Restrict your use of let to when you actually need a variable to be block scoped
  2. Do manual hoisting — move the let declaration to the top of their scope — to avoid the temporal dead zone
  3. Add extra brackets to limit the amount of code in the same scope as the let declaration that can be incidentally impacted by this performance problem

console.time(‘var’);
for (var i = 0; i < 100000000; i++) {}
console.timeEnd(‘var’); // var performance is fast again! :D

console.time(‘let’);
{ // these extra brackets stop the TDZ performance problem from spreading
let j
for (j = 0; j < 100000000; j++) {}
}
console.timeEnd(‘let’);

When do I really need to use let?

The only pretty mandatory use case for let that I can think of is whenever you want an async action to happen in a loop, and you need fresh variable bindings for each iteration through the loop.

This example from the MDN highlights this problem very well…

var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"];

for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1500);
}

This code will console.log undefined three times, because i has been incremented to 3 before any of the callback functions from the setTimeouts actually execute. Declaring i as a let will solve this problem for us.

Email me at sbakkila@gmail.com if you can come up with any other solutions to the problems created by the temporal dead zone, or other situations where we absolutely need let!

--

--