1

I have an inner function that references a variable initialized by its containing outer function:

function outer() {
    function inner() {
      if (foo) { ... }
    }
    let foo = 'bar';
    inner();  // no error
};

However, there are some circumstances where the inner function may be invoked before the statement defining foo has executed. In this case, any reference to foo causes a ReferenceError in the inner function:

function outer() {
    function inner() {
      if (foo) { ... } // ReferenceError: foo is not defined
    }
    inner();
    let foo = 'bar';
};

This is surprising to me, given that foo has block scope, and I am within the enclosing block when executing the inner function.

More surprisingly is that even attempting to detect this situation with the typeof operator - which I always understood to be a safe way to test for undefined variables - causes the same ReferenceError:

function outer() {
    function inner() {
      if (typeof foo !== 'undefined') { ... } // ReferenceError: foo is not defined
    }
    inner();
    let foo = 'bar';
};

Edit: I now understand that this behavior is the result of the "temporal dead zone" involving let and const variables discussed elsewhere. However, I'm still looking for a clean, safe way to handle the situation.

Is there any safe way to test whether a block-scoped variable (e.g., one created with 'let' or 'const') has yet reached its declaration?

Myk Willis
  • 12,306
  • 4
  • 45
  • 62
  • Use this keyword => this.foo to make sure the variable declared or not. – Nattamai Jawaharlal Manikandan Nov 11 '18 at 20:51
  • @NattamaiJawaharlalManikandan The _property_ `this.foo` would be something completely distinct from the _variable_ defined in this scope. – Myk Willis Nov 11 '18 at 21:50
  • "*Is there any safe way to test whether a block-scoped variable has yet reached its initialisation?*" - no, not really, except for `try`/`catch`. But what would you need this for? Either you will want to use the variable or you don't. If it throws an error because you try to access it before it is initialised, don't work around it but just fix your mistake - calling `inner()` in the last line. – Bergi Nov 11 '18 at 22:29
  • @Bergi The reason this is needed in "real" code is because inner() is an error handling function, and outer() is a request processing function. In inner() (the error handling function), I want to gather all of the context information collected by outer() so that I can write it to an error response. `foo`, in the real world, is a variable that is the result of parsing input values to outer(), but there are conditions where we encounter an error before `foo` is initialized. Thus, inner() may have to be called before foo is initialized. – Myk Willis Nov 13 '18 at 06:40
  • @MykWillis Can you make an example of how you are detecting the error? Is it a `try`-`catch`? I'm unclear what the exact scope of `inner` and `foo` would be, wouldn't they be nested in different blocks? Also for what you described, I would recommend to simply use `var` (or put the `let` declarations at the top of your scope). – Bergi Nov 13 '18 at 08:39
  • @Bergi In my production code, outer() is a handler for a web request. It uses input from the request to look up several different records from databases. `foo` is one such record. There are lots of logical errors that can occur while processing the request, both before and after the lookup of `foo`. If an error happens at any point, we want to return an error response to the caller that includes as much context as possible to aid in debugging. This is done with `inner`. Yes, easiest thing to do is just use `var` or equivalent but I thought it an interesting language question. – Myk Willis Nov 13 '18 at 19:33

2 Answers2

2

One brute-force approach is to simply catch the exception thrown by typeof in the "temporal dead zone" before foo has been initialized:

function outer() {
    function inner() {
      let fooExists = false;
      try { fooExists = typeof foo !== 'undefined' } catch(e) {}
      if (fooExists) { /* ... */ }
    }

    inner();   // no error
    let foo = 'bar';
}

It is also possible to use var instead of let to work around this issue. Because var is function-scoped, the declaration will be hoisted to the top of outer, making the variable available (though undefined) at all times outer is executing:

function outer() {
    // due to hoisting, there is a logical `var foo;` here
    function inner() {
      if (typeof foo !== 'undefined') { /* ... */ }
    }

    inner();   // no error; `foo` has been hoisted
    var foo = 'bar';
}

A similar approach could be taken by putting the declaration (but not initialization) of foo at the top of the outer function while still using let:

function outer() {
    let foo;
    function inner() {
      if (typeof foo !== 'undefined') { /* ... */ }
    }

    inner();   // no error
    foo = 'bar';
}

This last solution would seem to be the cleanest solution for the example given in the question. Unfortunately, it cannot be used when using const variables.

Myk Willis
  • 12,306
  • 4
  • 45
  • 62
-1
function outer() {
    function inner() {
      if (foo) { ... } // ReferenceError: foo is not defined
    }
    inner();
    let foo = 'bar';
};

This is surprising to me, given that foo has block scope, and I am within the enclosing block when executing the inner function.

Because of hoisting, the declaration for foo is hoisted to the top of the block, but not the initialization of the variable. It executes as if you had written:

function outer() {
    let foo; // declaration only, so still undefined value
    function inner() {
      if (foo) { ... } // ReferenceError: foo is not defined
    }
    inner();
    foo = 'bar'; // Not initialized until after you call inner!
};

Simply move the initialization up in the block and it will work:

function outer() {
    let foo = 'bar'; // declared and initialized
    function inner() {
      if (foo) { ... } // works as expected
    }
    inner();
};
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
  • 2
    The declaration of `foo` is apparently not logically moved to the top of the outer() function (because if it was, `typeof foo` would return 'undefined' without an exception). This _would be_ the behavior if I had used `var` to define `foo`, but in the case of `let` (and `const`) it is apparently not so. e.g., https://stackoverflow.com/a/31222689/925478 – Myk Willis Nov 11 '18 at 21:42
  • And as for moving the initialization to the top of outer(), that is unfortunately not possible in the actual production code I have, which initializes `foo` based on a function call that is only performed after a bunch of preprocessing work. e.g., we don't know the value to which it should be initialized until far down in the function. – Myk Willis Nov 11 '18 at 21:43
  • 2
    *"It executes as if you had written"* - This is incorrect. One will give an error, the other will not. – Spencer Wieczorek Nov 11 '18 at 21:43
  • @MykWillis The declaration of foo is moved to the top of the block (hoisted) as referenced by your link as well as [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#Temporal_dead_zone). It's just that `let` scoped variables have a "temporal dead zone" which prevents access to them prior to initialization. – Scott Marcus Nov 11 '18 at 22:17
  • @SpencerWieczorek I meant it processes logically as if it was written the way I showed. In other words, it's hoisted. The fact that it throws an error doesn't preclude hoisting. – Scott Marcus Nov 11 '18 at 22:18