In general, this is how hoisting works:
- the declaration of the variable is moved to the top
- the variable is initialized with a special "hoisted" value
- when the program reaches the
var/let/const line, the variable is re-initialized with the value mentioned on that line (or undefined if there's none).
Now, your example can be simplified down to this:
console.log(a)
let a = 150
which is actually:
a = <hoisted value>
console.log(a)
a = 150
It throws an error because, for let and const, the hoisted value is a special object that raises an error when you try to access it.
On the other side, the hoisted value for var is just undefined, so this will print undefined without throwing an error:
console.log(a)
var a = 150
Also, there's some confusion (including this very thread) about which variable types are hoisted, and a so-called "dead zone" for let/const vars. It's simpler to think of things this way: everything is hoisted, that is, all variable bindings in a block are created before entering the block. The only difference between var and let/const in this regard is that with the latter you are not allowed to use a binding until you initialize it with a value.
See https://stackoverflow.com/a/31222689/989121 for more details.