2

I was messing around in the JS console and I stumbled across some perplexing behavior. The second error (SyntaxError) makes sense to me, I already declared (or tried to at least) bar so I shouldn't be able to declare it again. However, I would expect bar to be undefined in this case.

How can a variable be both declared and not defined? Can anyone explain what is going on internally?

let bar = fo.map(i => console.log(i)) //typo
VM2927:1 Uncaught ReferenceError: fo is not defined
    at <anonymous>:1:11
(anonymous) @ VM2927:1

let bar = foo.map(i => console.log(i)) //fix typo
VM2999:1 Uncaught SyntaxError: Identifier 'bar' has already been declared
    at <anonymous>:1:1
(anonymous) @ VM2999:1

bar
VM3019:1 Uncaught ReferenceError: bar is not defined
    at <anonymous>:1:1

.as-console-wrapper {max-height: 100% !important;}
<script>
  //typo:
  let bar = fo.map(i => console.log(i)) //Uncaught ReferenceError: fo is not defined
</script>
<script>
  //fix typo:
  let bar = foo.map(i => console.log(i)) //Uncaught SyntaxError: Identifier 'bar' has already been declared
</script>
<script>
  console.log(bar) //Uncaught ReferenceError: bar is not defined
</script>
FZs
  • 16,581
  • 13
  • 41
  • 50
jjjjjj
  • 23
  • 5
  • 2
    I think this is mostly a console artifact, I'm not sure how it affects real programs. – Barmar Jan 15 '20 at 21:43
  • it is true... if I say `let b = adlajdlakjsd;` and it errored out, and now `let b = 1` causes error (already declared) and if I print out `b`, I can't (undefined error), if I do `b = 2`, I can't (undefined error). So what is the state of `b`? Cannot set it, and cannot declare it again, what state is it in? So you have to quit Node and restart it, or reload the page of your browser to be able to use `b`? What if the next 20 or 50 lines of code you already have in editor has `b`? Then you have to replace them to `b2`? – nonopolarity Jan 15 '20 at 21:50
  • 1
    I've seen an identical question recently. I can't find it, unfortunately. @T.J.Crowder, @CertainPerformance or someone else with high JS rep answered that. It is how the browser console works (and also Node in terminal). In this case, `bar` has been declared but during value assignment, an error occurred so now it is in a specific state and it's neither declared nor undeclared, neither defined nor undefined. After browser refresh, you will be able to do that again – Sebastian Kaczmarek Jan 15 '20 at 21:59
  • 1
    I've been trained by my own crappy typing ability to use `var` rather than `let` in interactive contexts – danh Feb 01 '20 at 20:52

2 Answers2

4

Actually, it can't at all.

TL;DR: In your code, the variable bar is declared and defined, but not initialized.


The first error is right: bar was declared twice.

Also note, that it's a compile-time SyntaxError, so it happens before the code gets evaluated, so it isn't affected by the thrown exception inside the variable declaration:

//SyntaxError

console.log('Evaluating code') //Never runs
let foo = 'bar'
let foo = 'baz'

But the second error isn't so obvious: why isn't bar just undefined?

After a lot of searching in the ECMAScript 6 specification, I've found the source of the problem. That's a "bug" (or at least a situation that hasn't taken care of) in the spec itself, but fortunately, it's very rare outside of a JS console.

You may know, that let and const variables have a so-called temporal dead zone, that throws ReferenceErrors when you try to look up or assign to variables before they're declared:

/* Just to make console fill the available space */
.as-console-wrapper{max-height:100% !important;}
<!-- Using separate scripts to show all errors -->
<script>
  console.log(foo) //ReferenceError
  const foo = 'bar'
</script>
<script>
  bar = 'baz' //ReferenceError
  let bar
</script>

That's because these variables' bindings are created before the containing code block get executed, but aren't initialized until the variable declaration statement evaluated. Trying to retrieve or modify the value of an uninitialized binding always results in a ReferenceError.

Now, let's inspect the evaluation of the let (and const) statement's LexicalBinding (variable = value pair), it's defined as follows:

LexicalBinding : BindingIdentifier Initializer

  1. Let bindingId be StringValue of BindingIdentifier.
  2. Let lhs be ResolveBinding(bindingId).
  3. Let rhs be the result of evaluating Initializer.
  4. Let value be GetValue(rhs).
  5. ReturnIfAbrupt(value).
  6. If IsAnonymousFunctionDefinition(Initializer) is true, then
    1. Let hasNameProperty be HasOwnProperty(value, "name").
    2. ReturnIfAbrupt(hasNameProperty).
    3. If hasNameProperty is false, perform SetFunctionName(value, bindingId).
  7. Return InitializeReferencedBinding(lhs, value).

Emphasis mine.

Without really going into detail, I'd like to highlight the most important things:

  • BindingIdentifier is the variable name
  • Initializer is the value to assign to it
  • ReturnIfAbrupt() is an abstract algorithm, that returns from its caller with its argument if its argument is a Completion Record that represents an abrupt completion (e.g. a thrown exception)
  • InitializeReferencedBinding() initializes the given Binding

The problem appears when an exception is thrown during the evaluation of the Initializer.

When that happens, this:

GetValue(rhs)

...will return an abrupt completion, so the following line:

ReturnIfAbrupt(value)

...returns from the let (or const) statement with the abrupt completion record (i.e. re-throws the exception), so that line:

InitializeReferencedBinding(lhs, value)

...won't run at all, therefore, the variable's binding remains uninitialized and continues to throw ReferenceErrors when you try to look it up or assign to it.

These errors' message (foo is not defined) is even more confusing and inappropriate, but that depends on the implementation, so I can't reason about it; however, probably it's because of another unhandled case.

FZs
  • 16,581
  • 13
  • 41
  • 50
  • It's something of a paradox that the primary reason for using *let* and *const* is to minimise code errors from accidental identifier reuse or reassignment respectively, yet those keywords introduce new types of code error that do not exist for *var*. – RobG Jun 06 '21 at 04:16
  • @RobG I agree with that. However, note that this is impossible outside the console (unless you use globals and an `uncaughtException` event to keep the process alive - which is already called "unsafe"). It's not possible because the scope the variable was declared in is **always exited** because of the exception (even if it's in a `try` block, because `let` and `const` are *block*-scoped), making the variable inaccessible. – FZs Jun 06 '21 at 06:01
2

let foo = undefined; this is a declared undefined variable, if you use it somewhere you will get foo is undefined if you try to declare it again you will get an error SyntaxError: redeclaration, some functions return undefined when they fail, the variable you use to store the return value will be declared and undefined in the same time. in this example you can use foo but you can't re-declare it for example let foo = undefined; foo = 5;

phoenixstudio
  • 1,776
  • 1
  • 14
  • 19
  • 1
    But you don't get an error if you try to use the variable after `let foo = undefined;` – Barmar Jan 15 '20 at 21:41
  • *foo* is assigned the *undefined* value in the initialiser. It is only undefined before the assignment, after that its value is defined (as the *undefined* value). – RobG Jun 06 '21 at 04:18