JavaScript ES6 Modules: Understanding import & export

JavaScript ES6 Modules: Understanding import & export

One of the biggest upgrades in modern JavaScript came with ES6 Modules.

They didn’t just add new syntax — they changed how we structure, scale, and reason about JavaScript applications.

The Problem

Before ES6 Modules there exists, a single massive JavaScript file, in which things quickly become unmanageable. Every feature, helper function, and variable lives in the same place, making the file hard to read, harder to debug, and risky to change. A small modification in one part can unintentionally break something elsewhere.

Then there are global variables, where it’s often unclear which part of the code modified them. Before ES6 Modules, variables declared in one file often leaked into the global scope. This means:

  • different scripts could overwrite each other’s variables
  • bugs appeared without an obvious source
  • behavior changed based on script loading order

These bugs were especially painful because the code looked correct, but failed at runtime.

Also, tightly coupled code made reuse almost impossible. Functions depended on shared globals instead of explicit inputs. We couldn’t move logic to another file or reuse it in a different project without dragging half the codebase along with it.


How ES6 Modules Fix This

ES6 Modules solve all three problems by introducing explicit boundaries:

  • Each file has its own scope (no accidental globals)
  • Dependencies are clearly declared using import
  • Public APIs are intentionally exposed using export

This means:

  • Code becomes modular and predictable
  • Changes are localized and safer
  • Logic becomes reusable and testable

Instead of one giant script controlling everything, our application becomes a collection of small, focused modules that work together — which is exactly how modern JavaScript applications are built.


What Are ES6 Modules?

An ES6 Module is simply a JavaScript file that:

  • has its own scope
  • can export values
  • can import values from other files

Each file becomes a self-contained unit of logic.

// math.js
export const add = (a, b) => a + b;        
// index.js
import { add } from "./math.js";        

This small change enables clean architecture in JavaScript.


Why Do We Need Modules?

Before ES6, JavaScript didn’t have a native module system. Everything lived in the global scope, and that shaped how applications were written.

  • Global variables : Scripts exposed variables and functions directly on the global object (window). As projects grew, name collisions became common, and tracking who changed what turned into a debugging nightmare.
  • Script loading order : Dependencies were managed by the order of <script> tags. If one file depended on another, we had to load them in the correct sequence. A small mistake in ordering could break the entire app at runtime.
  • IIFEs (Immediately Invoked Function Expressions) : Developers wrapped code in IIFEs to create private scopes and avoid polluting the global namespace. This helped, but it was more of a workaround than a real solution—harder to read, harder to share logic, and not truly modular.

This approach worked for small scripts and simple pages, but as applications scaled:

  • code became tightly coupled
  • reuse was difficult
  • maintenance and collaboration suffered

That growing pain is exactly why ES6 Modules were introduced—to bring structure, isolation, and predictable dependency management to JavaScript.

What ES6 Modules solved:

With ES6 Modules in place, JavaScript unlocked several powerful benefits that simply weren’t possible before:

Predictable dependencies : Dependencies are declared explicitly using import and export. You can instantly see what a file needs and what it exposes. No more guessing based on script order or hunting through globals—the module graph is clear, deterministic, and easy to reason about.

Better tooling : Because the dependency graph is static and analyzable, tools like bundlers, linters, and IDEs can work smarter. This enables:

  • accurate autocomplete and refactoring
  • dead-code detection
  • safer renaming and navigation

Tree-shaking & optimization : Since ES6 modules use static imports, bundlers can remove unused exports during build time. If you import only one function from a library, the rest can be excluded from the final bundle—resulting in smaller files, faster loads, and better performance, especially for large applications.

Together, these features transformed JavaScript from “script-based” code into a scalable, production-ready module system.


Exporting in ES6

1. Named Exports

Used when a module exposes multiple utilities.

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;        

We can also export at the bottom:

const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;

export { multiply, divide };        

2. Default Export

Used when the module has one primary responsibility.

// calculator.js
export default function calculate(a, b) {
   return a + b;
}        
A module can have only one default export.

Importing in ES6

1. Import Named Exports

import { add, subtract } from "./math.js";        

2. Import with Alias

import { addas sum } from "./math.js";        

3. Import Default Export

import calculate from "./calculator.js";        

4. Import Everything

import * as math from "./math.js";

math.add(2,3);        

Module Scope (Very Important)

Each ES module has its own private scope.

// config.js
const secret ="123"; // private
export const apiKey = "abc"; // public        

Unlike classic scripts:

  • variables are not global
  • only exported values are accessible

This is one of the biggest architectural wins of ES6 Modules.


How ES6 Modules Work Internally

>> Modules are loaded only once

When a module is imported, JavaScript loads and executes it a single time. If multiple files import the same module, they all share the same instance.

// config.js
export let count = 0;
export function increment() {
   count++;
}        
// a.js
import { increment } from "./config.js";
increment();        
// b.js
import { increment } from "./config.js";
increment();        

count becomes 2

The module is not re-executed for each import

This behavior allows modules to safely maintain shared state.


>> Imports are live bindings (not copies)

When you import something, you’re not getting a copy of its value — you’re getting a live reference to it. If the exported value changes, all imports see the updated value.

// state.js
export let value = 10;
export function update() {
   value = 20;
}        
// app.js
import { value, update } from "./state.js";
console.log(value); // 10
update();
console.log(value); // 20        

This is why imported variables stay in sync across files.


>> Execution is deferred

ES modules are executed after the HTML is parsed, even without defer.

<script type = "module" src = "app.js"></script>
        

This means:

  • No blocking page rendering
  • DOM is available when module code runs
  • No need for DOMContentLoaded in many cases

It makes module scripts safer and more predictable.


>> Modules run in strict mode by default

All ES modules automatically run in JavaScript strict mode. This prevents common mistakes:

x = 10; // ❌ ReferenceError (no implicit globals)        
function test(a, a) {} // ❌ Duplicate parameters not allowed        

Benefits:

  • Fewer silent bugs
  • Better error reporting
  • Cleaner, more secure code

We don’t need to write:

"use strict";        

It’s already enforced.


Using ES Modules in the Browser

<script type = "module" src = "index.js"></script>        

This enables:

  • native import / export
  • deferred loading
  • better dependency management

Module scripts follow CORS rules.

ES Modules in Node.js

You can enable ES Modules by either:

  • using .mjs extension
  • OR adding this to package.json

{
   "type":"module"
}        

Now Node.js understands import / export natively.


ES Modules vs CommonJS

ES Modules (ESM) and CommonJS (CJS) differ significantly in their implementation of JavaScript modularity.

ESM uses import and export syntax to support static loading and native browser compatibility, whereas CJS relies on require and module.exports for dynamic loading, which lacks native browser support.

ES Modules enable tree shaking for better performance and operate in strict mode by default, CommonJS does not support tree shaking and requires strict mode to be enabled manually.

This is why modern tooling prefers ES Modules.


Real-World Usage

We use ES6 Modules without even noticing in:

  • React components
  • Angular services
  • Utility libraries
  • Node.js backends
  • Bundlers like Vite, Webpack, Rollup

They are the foundation of modern JavaScript architecture.



To view or add a comment, sign in

More articles by Akash Kumar

Others also viewed

Explore content categories