Yes, they "hoist" but as unaccessible thing which always throws an error when read or written. It's called the "temporal dead zone".
Further reading: https://medium.com/nmc-techblog/advanced-javascript-es6-temporal-dead-zone-default-parameters-and-let-vs-var-deep-dive-ca588fcde21b
Conceptually, for example, a strict definition of hoisting suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your code. [Hositing]
[...]
For most of the ES6 features (let, const, default parameters, etc), The Creation Phase work quite differently. It still goes through the code and allocates space for variables, but the initializer set the variables with a special mode called TDZ (Temporal Dead Zone), which means the variables exist but you can’t access them until you assign some value.
So you can imagine it like this:
let X = TDZ;
console.log(X); // error
X = undefined; // in your code: let X;
...compared to normal hosting behavior:
var X = undefined;
console.log(X); // undefined
X = whatever; // in your code: var X = whatever;
Of course this is not 100% right because you also can't write X = 123
before the let
, and there is no valid JS that would describe an "unwritable variable". But I think you get the idea.
In the ECMAScript 2021 Language Specification this is described in 13.3.1 as follows (it seems it doesn't use the term "TDZ" there, although I have heard this name used many timed before, it's also used in MDN):
13.3.1 Let and Const Declarations
NOTE
let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Environment Record is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
This means that at the start of your block, the Environment Record is instantiated - the variable "exists" but is in the TDZ. As said here, it cannot be accessed, hence the error. Once the let
line is executed, its LexicalBinding is evaluated and the variable comes out of the TDZ and is now accessible. Since you didn't specify an initializer, its value is now undefined
.