Ad – 728Γ—90
πŸ”§ Intermediate JS

JavaScript Modules – import and export

Modules let you split JavaScript code into separate files, each with its own scope. Variables and functions inside a module are private by default β€” you choose exactly what to export and what to keep internal. This eliminates global namespace pollution and makes large codebases manageable.

⏱️ 22 min read 🎯 Intermediate πŸ“… Updated 2026

Why Modules?

Before modules, all JavaScript shared a single global scope. Every variable and function was a potential name collision waiting to happen. Modules solve this with encapsulation, reusability, and explicit dependency management.

JavaScript
// Without modules – everything is global (old way)
// math.js (included via <script>)
var add = function(a, b) { return a + b; }; // pollutes global scope

// With modules – private by default
// math.js
const PI = Math.PI; // private β€” not accessible outside this file

export function add(a, b) { return a + b; }         // named export
export function multiply(a, b) { return a * b; }    // named export
export const VERSION = '1.0.0';                      // export a value

Named Exports

JavaScript
// utils.js
export function formatDate(date) {
  return date.toLocaleDateString('en-GB');
}

export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export const MAX_RETRIES = 3;

// Alternative: declare first, export at the end
function debounce(fn, delay) { /* ... */ }
function throttle(fn, limit) { /* ... */ }
export { debounce, throttle }; // export list

// Rename on export
export { debounce as debounceFn };

Default Exports

Each module may have at most one default export. Default exports are typically used for the "main thing" the module provides β€” a class, a function, or a configuration object.

JavaScript
// logger.js – a single default export
export default class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }
  log(msg) {
    console.log(`[${this.prefix}] ${msg}`);
  }
}

// api.js – export function as default
export default async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// config.js – export an object as default
export default {
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: { 'Content-Type': 'application/json' }
};

Importing

JavaScript
// Named imports – must match exported names
import { formatDate, capitalize, MAX_RETRIES } from './utils.js';

// Rename on import with 'as'
import { formatDate as fmt, capitalize as cap } from './utils.js';

// Import everything as a namespace object
import * as Utils from './utils.js';
Utils.formatDate(new Date());
Utils.capitalize('hello');

// Default import – name can be anything
import Logger from './logger.js';
import fetchUser from './api.js';

// Mix default and named imports
import Logger, { LOG_LEVELS } from './logger.js';

// Import for side effects only (e.g., polyfills, initialisation)
import './setup.js'; // runs the module code but imports nothing

// Using imports
const logger = new Logger('App');
logger.log('Server started'); // [App] Server started

const user = await fetchUser(42);
console.log(user.name);
β–Ά Output
[App] Server started
Alice   // user.name from fetchUser(42)
πŸ’‘
Named vs Default: Which to Use?

Prefer named exports for utility modules with multiple exports. Use a default export when a module has one primary responsibility. Avoid modules that mix many defaults β€” it makes tree shaking harder and imports confusing.

Re-Exporting and Barrel Files

JavaScript
// components/index.js – "barrel" file that re-exports
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';
export { default as Card } from './Card.js'; // re-export default as named

// Now consumers import from one clean path:
import { Button, Modal } from './components/index.js';
// instead of:
// import { Button } from './components/Button.js';
// import { Modal }  from './components/Modal.js';

// Re-export everything from a module
export * from './utils.js';
export * from './validators.js';

// Re-export with rename
export { formatDate as fmtDate } from './utils.js';

Dynamic import()

Static import is resolved at parse time. Dynamic import() loads a module at runtime and returns a Promise β€” perfect for code-splitting and lazy loading.

JavaScript
// Load a module only when needed
async function loadChart() {
  const { default: Chart } = await import('./Chart.js');
  return new Chart('canvas');
}

// Conditionally load based on feature detection
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  const { applyDarkTheme } = await import('./themes/dark.js');
  applyDarkTheme();
}

// Lazy-load on user action
document.getElementById('loadEditor').addEventListener('click', async () => {
  const { Editor } = await import('./Editor.js');
  const editor = new Editor('#container');
  editor.init();
});

// Import with error handling
try {
  const module = await import('./optional-feature.js');
  module.init();
} catch (err) {
  console.warn('Optional feature not available:', err.message);
}

Using Modules in HTML

JavaScript
// In HTML: add type="module"
// <script type="module" src="./main.js"></script>

// Key differences with type="module":
// 1. Deferred by default (runs after DOM is parsed)
// 2. Strict mode is always on
// 3. Has its own scope (no global leakage)
// 4. Can use top-level await

// main.js (type="module")
import { initRouter } from './router.js';
import { setupTheme } from './theme.js';

// Top-level await (ES2022 β€” works in module scripts)
const config = await fetch('/api/config').then(r => r.json());

initRouter(config.routes);
setupTheme(config.defaultTheme);

CommonJS vs ES Modules

FeatureCommonJS (CJS)ES Modules (ESM)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronous (runtime)Asynchronous (parse time)
Static analysisNot possibleYes β€” enables tree shaking
Top-level awaitNoYes (ESM only)
Default in Node.jsYes (legacy)Yes with .mjs or "type":"module"
Browser supportNo (needs bundler)Native (modern browsers)
Named exportsVia destructure of require()First-class syntax
Circular dependenciesHandled (with caveats)Handled (but avoid them)
πŸ’‘
Tree Shaking

Because ES module imports and exports are statically analysable, bundlers (Webpack, Rollup, Vite) can determine which exports are actually used and strip unused code from the final bundle. This is called tree shaking and only works with ESM β€” it cannot work with CommonJS require().

Interview Questions

  • What is the difference between a named export and a default export?
  • How does dynamic import() differ from static import?
  • Why do ES Modules enable tree shaking while CommonJS does not?
  • What happens when you have circular module dependencies?
  • What does import * as ns from './module.js' do?

πŸ‹οΈ Practical Exercise

Create a mini math library across two files. In arithmetic.js, export named functions add, subtract, multiply, and divide. In statistics.js, export mean, median, and mode using the arithmetic helpers. Then create an index.js barrel file that re-exports everything from both. Write a consumer script that imports { mean, multiply } from the barrel.

πŸ”₯ Challenge Exercise

Build a plugin system using dynamic import(). Define a PluginManager class that accepts plugin names, loads them lazily via import() from a plugins/ directory, registers their init() method, and calls all registered init methods when pluginManager.runAll() is called. Handle loading errors gracefully.

Frequently Asked Questions

Do I need a bundler to use ES Modules?
Not for modern browsers or Node.js β€” both support ESM natively. But for production web apps you still want a bundler (Vite, Webpack, Rollup) for code-splitting, minification, and compatibility.
Can a file have both a default and named exports?
Yes. A file can have exactly one default export and any number of named exports. Consumers can import both: import Default, { named1, named2 } from './module.js'.
Why do module imports need the .js extension?
In native ES Modules (browser and Node.js), file extensions are required because the runtime needs to know exactly what URL to fetch. Bundlers like Webpack and Vite often resolve extensions automatically, which is why you can omit them in framework code.
What is a circular dependency and why is it a problem?
A circular dependency occurs when module A imports from B, and B imports from A. JavaScript handles this by providing a partially-initialised export object, which can cause variables to be undefined when accessed. Avoid circular dependencies by restructuring shared code into a third module.
Is import() supported everywhere?
Yes, in all modern browsers and Node.js 12+. In very old environments, dynamic imports require a polyfill or bundler transformation.
Ad – 336Γ—280

πŸ“‹ Summary

  • Modules give each file its own scope β€” nothing leaks to the global namespace.
  • Use named exports for multiple exports; default export for the module's primary value.
  • Import with exact names for named exports; any name for default exports.
  • Use import * as ns to import everything into a namespace object.
  • Barrel files (index.js) consolidate re-exports for cleaner import paths.
  • Dynamic import() loads modules at runtime β€” enables lazy loading and code-splitting.
  • ESM enables tree shaking because imports are statically analysable at build time.
  • Add type="module" to script tags to use ES Modules in HTML; modules run deferred and in strict mode.