14

Recently I ran into this weird thing in chrome console. Here I am intentionally assigning an undefined thing to a in order to throw an error.

let a = werwr // Uncaught ReferenceError: werwr is not defined

Then when I tried to assign something legit to a, this happened:

let a = "legit string"   // Uncaught SyntaxError: Identifier 'a' has already been declared

so I can't use "let" because a has already been declared. So I tried to reassign something else to the "already declared a"

a = "legit string"   //  Uncaught ReferenceError: a is not defined

So it seems like I can't reassign something else to a but at the same time, a has been declared so I can't use let again.

I understand the difference between declaring and assigning a variable. However here it seems that neither could be done again. Does this has something to do with the scope of "let" in console? Because the same thing totally works for "var"

var a = werwr 
// Uncaught ReferenceError: werwr is not defined

a = ”legit string“  
// ”legit string“

var a = "legit string" 
// Uncaught SyntaxError: Identifier 'a' has already been declared

Follow-up

There seem to be some difference between "manually" hoisting the let statement vs the implicit case.

throw new Error
let example = 5
// same errors as before

while in this case example can be reassigned again.

let example
throw new Error
example = 5
whales
  • 787
  • 1
  • 12
  • 24

2 Answers2

10

This happens when you introduce the temporal dead zone to the global scope. As you might know, let declarations are hoisted but left uninitialised. Due to control flow, it can happen that a variable is never initialised:

function …() {
    if (false)
        example; // would throw a ReferenceError if it was evaluated
    … // do something
    if (true)
        return; // stop!
    let example = 5; // never executed
}

This is fine in a function scope. Maybe something went wrong, maybe the variable wasn't needed at all - in the next call, a new scope with a new variable will be created.

A similar thing can happen in the global scope, when you throw an exception before the variable is initialised (only exceptions work here as a control flow construct, nothing else achieves the same effect).

throw new Error;
let example = 5;

In contrast to the function scope, it does matter here that the variable stays uninitialised. The global scope lasts forever, and the variable is eternally dead. It was not and will never be initialised, and lexical variables cannot be re-declared (which helps preventing mistakes).

This was discussed on es-discuss, but deemed irrelevant. If top-level <script> execution throws an error, you have bigger problems than uninitialised variables. There is no path to recover. If you need one (e.g. by trying to re-declare it in successive scripts), you have to use var anyway.

That you have the same problem in the devtools console is a bit of a nuisance, but could be solved for the console as a special scope.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Considering that let is hoisted, I wrote the code like this: " let x; throw new Error; x = 6" in this case, x can actually be reassigned later. Is there any subtle difference between "manually" hoist the declaration statements vs the implicit implementation? I added follow-up to the question with better format – whales Dec 21 '16 at 08:54
  • Yes, `let x;` is a shortcut for `let x = undefined;`, and if you put that in the top manually you will have initialised the variable before the exception gets thrown. – Bergi Dec 21 '16 at 12:37
  • Re "There is no path to recover", in future it may be able to ` – Pacerier Nov 23 '17 at 21:46
  • @Pacerier You can *handle* the error (already today with `window.onerror`) and log it, but you cannot *recover* it. It's impossible to undo what the script has done prior to throwing the exception, and it's impossible to un-declare a variable. – Bergi Nov 23 '17 at 21:54
1

You should know about hoisting in JS. Basically, a declaration like this let a = werwr;

is interpreted as let a; a = werwr;

And that why a is already declared when you run your second line of code.

UPDATE

So, there is a NOTE in ES specs https://tc39.github.io/ecma262/#prod-LetOrConst

let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Lexical Environment 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.

...

"but may not be accessed in any way until the variable's LexicalBinding is evaluated" means that, the declaration must be successfully completed before you can access the variable (either getting value, assigning value, or doing typeof, or event delete);

In your case, the variable's LexicalBinding is interupted by the exception.

let a = werwr // Uncaught ReferenceError: werwr is not defined

Please follow the link and read more about that. If you find a way to recover varaible a, please tell me. Thanks, today I found out something new about Javascript.

vothaison
  • 1,646
  • 15
  • 15
  • if that's the case, I should be able to do "a = "legit string", but it threw the error that a is not defined. also if I write the statements separately like you did, I could reassign a. However in my case, it's not happening – whales Dec 21 '16 at 04:47
  • You're right, my final explanation now is Chrome developer tool run your script by eval(). I am trying to make a example out of that. But, with `let` inside an eval().... Well. Good luck. – vothaison Dec 21 '16 at 05:06
  • 1
    thanks for doing the research. Bergi's answer was pretty thorough if you are interested – whales Dec 21 '16 at 08:24