Temporal Dead Zone - the less obvious part and how to avoid it
Tags: javascript, typescript, programming, webdev
Published: 2023-03-02 10:33:06

`let` and `const` bindings in JavaScript have a property called Temporal Dead Zone (TDZ). It’s a deadly trap indeed, and often not easy to spot. I learned this the hard way some time ago when working on JavaScript bundle optimization at Hotjar.

## What is a Temporal Dead Zone?

According to MDN:

>  A `let` or `const` variable is said to be in a ‘Temporal Dead Zone’ from the start of the block until code execution reaches the line where the variable is declared and initialized.

```typescript
{ // TDZ starts at beginning of scope
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}
```

Okay, this is obvious—you cannot access a constant before you declare it. But nobody writes code like this!, you say. Moreover, ESLint will yell at you if you try. What’s the big deal, then? 

Consider a more complex example.

```typescript
const recurse = (() => {
  if (recurse) { // throws an error, it's a TDZ for recurse
    recurse();
  }
})();
```

Expressions in JavaScript are evaluated in a certain order. Assigning a value is an expression evaluated from right to left. That’s why `const a = 1 + 2` equals `a === 3`. So, in the example above, first, the IIFE is evaluated, then the recurse name is initialized. That makes the usage of recurse inside the IIFE forbidden. Even if the name can be seen earlier in the code, from the program evaluation flow, it’s no go. But, again, nobody writes code like this, right? And ESLint is smart enough to warn you if you try. What’s your problem, dude?

## When ESLint can’t help you

ESLint analyzes the code statically. It’s smart, but because it doesn’t understand all the nuances of the runtime flow, a code like the one below is perfectly valid for the checker.

```typescript
const callCallback = (cb) => {
  cb();
  return () => {
    console.log('Returned function called!');
  };
};

const returned = callCallback(() => {
  returned();  // throws an error, it's a TDZ for returned
});
```

## A real-life example

It all seems made-up: do we use patterns like this in our codebase? It turns out they’re rare, but they’re there. And they look completely innocent. 

Take a look at what I found in Hotjar’s codebase:

```typescript
const unsubscribe = useStore.subscribe((state: Store) => {
  // ...
  if (unsubscribe) unsubscribe();
  // ...
});
```

This code may feel illogical, but the `subscribe` method is a bit more complex than the `callCallback` you could see above. It calls the callback asynchronously most of the time, but in certain cases, it does it synchronously. And this leads to a runtime error about an uninitialized variable when you try to unsubscribe inside the callback.

![A screenshot from browser DevTools showing an error trigger when a binding is accessed while in TDZ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56juotydyt8gh2h6w94x.png)


## But… I’ve never encountered such an error

How could a code like this even slip into the main branch? It’s broken, and loading it in the browser or running a test would detect it immediately, right? It would—if we actually used `const` and `let` bindings. But for a long time, this wasn’t the case at Hotjar, and we didn’t realize it!

Despite this syntax having been the norm here for years now, with several  such bindings present in our codebase, so far, all of them have been transpiled to `var` before they reached the browser. For these, hoisting mechanism kicks in and silently evaluates the uninitialized binding to `undefined`. If you’ve only read ‘JavaScript: The Good Parts’, you may even forget this mechanism exists.

![Illustration of two books - JavaScript: The Good Parts and JavaScript: The Definitive Guide - put on top of each other](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/unuek4s6zn9pz54iamml.png)
(_image source:_ https://blog.klipse.tech/javascript/2016/09/21/valueOf-js.html)

At Hotjar, the transpilation step was there because we were supporting IE 11 and other ‘odd, old browsers’ that didn’t understand the `const` and `let` syntax. It was replaced with `var` on CI (via Babel), in a way engineers working in product squads weren’t noticing. Noone could see errors like the one above when testing the application.

Things changed, though. Last year we officially dropped support for IE 11 in our product, which opened possibilities for dropping some compilation steps. It not only made our CI and development environment faster, but also had benefits for customers. ES6+ code is more terse, thus takes less time to load. The side-effect is that `const`s and `let`s are directly executed in the browser. So now, Hotjar engineers can, and certainly will, spot these errors occasionally. They’ve always been there, but hidden!

## How to detect TDZ access

In the example above, failing E2E (we use Cypress) tests for some areas of our codebase uncovered the issue. This was great, but also strange (and a bit concerning) at the same time: no other check—linter, type check, or unit tests—failed. This proves that some mistakes can be only spotted in runtime. Different test types are complementary and cannot replace each other.

## How to prevent TDZ access 

Use let, whenever there is a risk of accessing a binding before its initialization ends.

```typescript
// first, initialize empty let binding with null
let unsubscribe: ReturnType<typeof useStore.subscribe> | null = null;
// then, assign it 
unsubscribe = useStore.subscribe((state: Store) => {
  // ...
  if (unsubscribe) unsubscribe();
  // ...
});
```

The code is now even clearer and more explicit. Looking at the original version, you might not understand why we need to check if the `unsubscribe` isn’t empty before calling it. After all, its type, according to TypeScript, would always be `() => void`, no? But as we’ve proven, this wasn’t true. After switching to `let`, it’s obvious that, in some particular moment, this name could contain `null`.

## Takeaway

Watch out for Temporal Dead Zones even if ESLint doesn’t warn you! Write tests, including E2E ones. And make sure you’re


![🎶 Stayin’ alive 🎶 - a fragment of "Stayin’ Alive" music video from Bee Gees](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vknr4dgsxtbz0plry5qk.png)
(_image source:_ https://giphy.com)