Ilya Loskutov
Ilya Loskutov

Reputation: 2201

How is it possible an object has a property but access to it results in ReferenceError?

// a.js
import * as bModule from './b.js'
export const b = bModule

// b.js
import * as aModule from './a.js'
export const a = aModule

If a.js is an entry point of our app then b.js would run firstly. Calling of console.log(aModule) from b.js at this stage results in such output:

[Module] {
  b: <uninitialized>,
}

And if b.js try to access aModule.b it will end up with the ReferenceError: b is not defined.

ReferenceError is thrown when a code uses an identifier (variable) hasn't declared yet. However, in this case aModule does have the property b (the fact is borne out by console.log). Moreover, when we access an uninitialized object property we just get the undefined value but not the exception.

So how this behavior can be understood? Is it specified?

Upvotes: 1

Views: 57

Answers (2)

T.J. Crowder
T.J. Crowder

Reputation: 1075209

ReferenceError is thrown when a code uses an identifier (variable) hasn't declared yet.

Not only then. :-)

Moreover, when we access an uninitialized object property we just get the undefined value but not the exception.

Yes, but modules are modern constructs, and you're using those another modern construct: let. It's defined to fail if used before it could be initialized, rather than provide ambiguous values.

Yes, it's specified behavior. Modules go through a series of phases, and during that process their exports of let, const, or class bindings are created as uninitialized bindings, and then later those exports are initialized with values. When modules have cycles (circular dependencies), it's possible to see an export before it's initialized, causing a ReferenceError.

It's the same Temporal Dead Zone (TDZ) we can see here:

let a = 21;
console.log(a); // 21
if (true) {
    console.log(a); // ReferenceError
    let a = 42;
}

The declaration for a within the if block declares it throughout the block, but you can't access it until the declaration is reached in the step-by-step execution of the code. You can see that the declaration takes effect throughout the block by the fact that we can't access the outer a from inside the block.

The same TDZ applies to exports that haven't been initialized yet.

You'll only have this problem with top-level code in a module, since the module graph will be fully resolved before any callbacks occur. Your code in b.js would be safe using a.b in a function called in response to an event (or called from a.js).

If you needed to use a.b in b.js at the top level, you'd need to run the call after b.js's top-level code execution was complete, which you can do via setTimeout or similar:

setTimeout(() => {
    console.log(a.b); // Shows the `b` module namespace object's contents
}, 0);

Upvotes: 2

Yousaf
Yousaf

Reputation: 29314

This behavior is because of the temporal dead zone.

Unlike identifiers declared using var, identifiers declared using let or const are marked as "not yet initialized" and they can't be accessed until their declaration has actually executed during step-by-step execution of the javascript code.

Since b in a.js is defined with const, it is hoisted but you can't access it before its declaration has actually executed. Same would be the case if b was declared with let.

If you declare b using var, then you would see undefined as the output.

Upvotes: 3

Related Questions