
A deep, practical explanation of JavaScript ES Modules: execution model, shared exports, strict mode, browser rules, and real-world implications.
Martin Ferret
March 3, 2026
For years, JavaScript grew without a native module system. Early applications were small, scripts lived in the global scope, and naming conflicts were common. As applications became larger and more complex, this approach quickly reached its limits.
Today, JavaScript modules (ES Modules) are everywhere: in browsers, Node.js, and every modern framework and bundler. Yet many developers use them daily without fully understanding how they actually work.
This article breaks down JavaScript modules from first principles, focusing on what is true, what matters, and why it works this way.
A JavaScript module is simply a file.
Each file is an isolated unit with:
Modules communicate only through:
export → what a module exposesimport → what a module consumes
`// sayHi.js
export function sayHi(name) {
return Hello ${name};
}`
`// main.js
import { sayHi } from './sayHi.js';`
`console.log(sayHi('John'));`
No implicit globals. No hidden dependencies. Everything is explicit.

Before native modules:
Modules solved these problems by enforcing:
This is not syntactic sugar: it is a structural foundation for large applications.
Modules automatically run in strict mode.
x = 10; // ❌ ReferenceError
This prevents silent errors and enforces safer code by default.
Top-level variables are never global.
`// user.js
export const user = 'John';`
`// app.js
import { user } from './user.js';
console.log(user);`
Without export and import, nothing is shared.
Even multiple <script type="module"> tags on the same page do not share scope.
No matter how many times a module is imported, its top-level code runs only once.
`// logger.js
console.log('Module initialized');`
`import './logger.js';
import './logger.js';`
Output:
Module initialized
This behavior is intentional and fundamental.
Exports are live bindings, not snapshots.
`// config.js
export const config = {};`
`// init.js
import { config } from './config.js';
config.user = 'Pete';`
`// feature.js
import { config } from './config.js';
console.log(config.user); // Pete`
All importers receive the same reference.
This enables controlled configuration and shared state, when used deliberately.
type="module" Is RequiredBrowsers only recognize modules when explicitly declared.
<script type="module" src="main.js"></script>
Without it, import and export will fail.
Module scripts:
This makes them behave similarly to defer, but by default.
This is invalid in the browser:
import { tool } from 'myLib'; // ❌
A path is required:
import { tool } from './myLib.js'; // ✅
Bare imports are resolved by:
Browsers do not resolve them natively.
ES modules do not work over file://.
You must use:
This is a security requirement, not a limitation.
import.meta: Module ContextEach module has access to metadata about itself:
console.log(import.meta.url);
Useful for:
Even with native modules, bundlers remain essential in production.
They provide:
In real-world applications, native modules are the input, bundlers are the output.
Modules enforce architectural clarity:
If a codebase feels messy with modules,
the issue is not the module system, it’s the architecture.
JavaScript modules are not an optional modern feature.
They are the foundation of contemporary JavaScript.
Understanding:
is what separates developers who use modules
from developers who control them.
Ready to prove your JavaScript skills?
Get certified in JavaScriptKeep learning JavaScript. Then get certified when you are ready.

DOM Events & Modern Patterns Cheat Sheet
Learn modern DOM event handling, including propagation, delegation, listener cleanup, custom events, forms, state management, efficient mutations, and observers to build responsive, performant pages without leaks.
Martin Ferret

this in JavaScript: Binding Rules and Arrow Functions
JavaScript’s this depends on the call site, with explicit binding and new taking precedence; arrow functions inherit this, making them useful for callbacks but unsuitable as object methods.
Martin Ferret

Iterator Helpers: Lazy Pipelines in JavaScript
ES2025 added map, filter, take and drop to iterators. Learn how lazy pipelines skip intermediate arrays, handle infinite sequences, and where they do not help.
Martin Ferret