Skip to content

JavaScript Tutorial Modules

Module Scope and Shared State

Lesson 170 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Top-level variables are private

In a classic script, a top-level var or function becomes global. In a module, top-level declarations belong to the module and do not leak.

counter.js
JS
let count = 0;                 // private to this module

export function increment() {
  count++;
  return count;
}

export function getCount() {
  return count;
}

Other files cannot touch count directly. They can only use the exported functions. That is encapsulation, and it is the reason modules replaced tricks like the IIFE.

A module runs once

The first time any file imports a module, the browser runs its top-level code. Later imports reuse the same instance.

index.html
HTML
<script type="module">
  const source = "data:text/javascript," + encodeURIComponent(`
    let count = 0;
    export const increment = () => ++count;
  `);

  const first = await import(source);
  const second = await import(source);   // same module instance

  first.increment();
  console.log(second.increment());       // → 2
  console.log(first === second);         // → true
</script>

Both imports share one count. This makes a module a simple singleton: a good place for a shared configuration object, a cache or a small store.

Strict mode

Modules always run in strict mode. Mistakes such as using an undeclared variable throw an error instead of silently creating a global.

index.html
HTML
<script type="module">
  try {
    undeclared = 5;
  } catch (error) {
    console.log(error.name);   // → ReferenceError
  }
</script>

this at the top level

In a module, this at the top level is undefined (not window).

Top-level await

Modules can use await at the top level, outside any function.

config.js
JS
const response = await fetch("/config.json");
export const config = await response.json();

Any module that imports config.js waits until it has loaded. Use it carefully, because it delays everything that depends on it.

Sharing state: the rules

  • Exported values are live bindings (see Named Exports), but importers cannot assign to them.
  • Exported objects can be changed by anyone who imports them, because they share the same object. Prefer exporting functions that change the state.
store.js
JS
const state = { user: null };

export const getUser = () => state.user;
export const setUser = user => { state.user = user; };

Circular imports

If a.js imports b.js and b.js imports a.js, it is a cycle. ES modules handle it, but you may read a value before it is initialised, giving confusing errors. Avoid cycles by moving shared code into a third module.

Globals still exist

Modules can use globals like window, document and console. Scope only affects your declarations.

lightbulb
One rule of thumb
Keep module-level state small and private, export functions to read and change it, and keep modules focused on one job.