Overview

An overview of the various scenarios

Overview of combinations

Scenariotype of exporttarget(in)directerrorhoistingmodule.exports of circular module
regulardefaultes5direct-no{}
regular es6defaultes6directy1no{ default: getterFn() }
regular es6 vardefaultes6directy1yes{ default: getterFn() }
regular indirectdefaultes5indirect, functionsy2no{ default: expectedFn() }
regular named exportsnamedes5directy3no{ [namedExportName]: getterFn() }
named exports es6namedes6directy3no{ [namedExportName]: getterFn() }
named indirectnamedes5indirect, functionpossibly4no{ [namedExportName]: getterFn() }
class instance regulardefaultes5directy5no{}
class instance nameddefaultes5directy3no{ [namedExportName]: getterFn() }
class instance indirect namednamedes5indirectpossibly4no{ [namedExportName]: getterFn() }
regular chunks entryPointsdefaultes5direct-no{}
regular chunks dynamic importdefaultes5direct-no{}
named chunks dynamic importnamedes5indirect dynamic importpossibly4no{ [namedExportName]: getterFn() }
named chunks dynamic import ESMnamedes66indirect dynamic importpossibly4no{ [namedExportName]: getterFn() }

Footnotes

  1. Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization 2

  2. Maximum call stack size exceeded

  3. Uncaught ReferenceError: Cannot access '{{namedExportName}}' before initialization 2 3

  4. depends on which named export is being called, if it returns a static value, no error or issues. If it calls a module within the circ dep chain, it will lead to Maximum call stack size exceeded 2 3 4

  5. cannot read {{propertyName}} of undefined. The issue here is that the class is not exported yet, due to preliminary module.exports = {}

  6. also setting output.module = true and experiments.outputModule = true

Legend

default vs named

What kind of ES Module export statement are we using.

Default: export default

Named: export myVar or export { foo: varFoo, bar: varBar }

This is important because circular dependencies with default exports are undefined when reading directly and using es5. This can lead to unexpected behavior, but will less likely lead to app-crashing errors.

Circular dependencies with named exports use getters and when used directly, will lead to Uncaught ReferenceError: Cannot access '{{namedExportName}}' before initialization errors which can crash your app if not caught properly.

target

Which Webpack output target are we setting?

The main difference here is that with es6 and later, default exports are treated as named exports, using a getter. This can lead to Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization

(in)direct

Are all modules processed by Webpack before reading imported values?

Important note: indirect is not the same as async. This is all about the synchronous order of execution due to importing of scripts.

The moment we access an imported value matters:

If we access a value from a circular module before that module is processed fully, it's exports will not be set yet. It will have the preliminary module.exports = {}.

However, if we read value(s) from a circular module until after all modules in the circ dep chain are finished, we are able to read from the module.exports with its default or named exports set correctly.

The finishing part is important because webpack holds a reference to a module's module.exports and that can change once processing all modules is done:

entry.js

_10
// needed to ensure right order of module processing for this example
_10
import './moduleA.js';
_10
_10
import { direct, indirectFn } from './moduleB.js'
_10
_10
// All imports have been resolved and processed
_10
console.log({direct, indirectFn: indirectFn() })

moduleA.js

_10
import { bStatic } from './moduleB'
_10
_10
const useStaticValueFromB = `${bStatic} imported from ./moduleB`;
_10
_10
export default useStaticValueFromB;

moduleB.js

_11
import useImportedValue from './moduleA';
_11
_11
// ModuleA's `module.exports` is preliminary set: `{}`
_11
const direct = useImportedValue; // const direct = undefined
_11
_11
// We only read `useImportedValue` when this function is called in `entry.js`
_11
const indirectFn = () => useImportedValue;
_11
_11
const bStatic = 'bStatic'
_11
_11
export {direct, indirectFn, bStatic};

In the above example, by the time that we call indirectFn() in entry.js all modules have been fully processed by Webpack and have their module.exports set to the correct value.

It is important to realize that module.exports for moduleA.js is different when reading it in moduleB.js in direct, vs reading it in entry.js after all imports have been resolved.

Conclusion

Hopefully these pages have given you a better understanding of the way circular dependencies are handled in Webpack.

And perhaps you've also learned a couple things about the module resolution system.

Here are some key take aways:

Module resolution

Circular Dependencies

Next

There's no next, but you can return to the first page