JavaScript 44 🧬 ES Modules — import/export
// math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
export default function multiply(a, b) {
return a * b;
}
// main.js
import multiply, { add, PI } from './math.js';
console.log(add(2, 3));
console.log(PI);
console.log(multiply(2, 3));
// Renaming imports
import { add as sum } from './math.js';
console.log(sum(1, 2));
// Namespace import
import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.PI);
console.log(math.default(2, 3));
// Dynamic import
const module = await import('./math.js');
console.log(module.add(1, 2));
ES Modules (ESM) are the official, standardized way to split JavaScript code into reusable files. They use import and export to share code between files, and they’ve replaced the older CommonJS (require/module.exports) approach in modern JavaScript.
Key point: ES Modules are static — imports are resolved at load time, not runtime (except for dynamic import()). Each module has its own scope, exports are live bindings, and this is undefined at the top level.
a – What are ES Modules
A module is a JavaScript file that has its own scope and can export values for other files to import. Before modules, everything ran in the global scope — leading to naming conflicts, load-order problems, and fragile code.
Why modules matter:
| Problem | Module solution |
|---|---|
| Global namespace pollution | Each module has its own scope |
| Load order matters | Imports are resolved automatically |
| Reusability | Export once, import anywhere |
| Maintainability | Split code into focused files |
| Tree shaking | Bundlers drop unused exports |
| Explicit dependencies | Imports show what a module needs |
Enabling ES Modules:
In the browser:
<script type="module" src="main.js"></script>
In Node.js:
- Use
.mjsextension, or - Set
"type": "module"inpackage.json, or - Run with
node --input-type=module
ES Modules vs CommonJS:
| Feature | ESM | CommonJS |
|---|---|---|
| Syntax | import / export | require / module.exports |
| Loading | Static | Dynamic |
| Async | Yes (import()) | Sync |
this at top | undefined | module.exports |
| Tree shaking | Yes | No |
| Browser support | Native | Needs bundler |
| Node.js | .mjs or "type": "module" | Default |
CommonJS example (for comparison):
// math.js (CommonJS)
const add = (a, b) => a + b;
module.exports = { add };
// main.js (CommonJS)
const { add } = require('./math');
console.log(add(1, 2));
ES Modules example:
// math.js (ESM)
export const add = (a, b) => a + b;
// main.js (ESM)
import { add } from './math.js';
console.log(add(1, 2));
Module scope:
// a.js
const secret = 'a';
// b.js
const secret = 'b'; // no conflict — different modules
// main.js
import './a.js';
import './b.js';
// Both loaded, no global collision
Variables in modules don’t leak to the global scope.
Modules are singletons:
A module is evaluated once, no matter how many times it’s imported. All importers share the same instance.
// counter.js
let count = 0;
export function increment() { return ++count; }
// a.js
import { increment } from './counter.js';
increment(); // 1
// b.js
import { increment } from './counter.js';
increment(); // 2 — same instance
Modules are deferred:
Module scripts execute after the HTML is parsed, like defer. They don’t block page rendering.
<script type="module" src="main.js"></script>
<!-- Runs after DOM is ready -->
Strict mode by default:
All ES Modules run in strict mode — no need for 'use strict'. Assigning to an undeclared variable throws.
// In a module
x = 10; // ReferenceError: x is not defined
Top-level this is undefined:
// In a module
console.log(this);
// [ undefined ]
// In a regular script
console.log(this);
// [ window ] (browser) or [ {} ] (Node.js)
Module scope vs global:
┌──────────────────────────────────────────────┐
│ Classic scripts │
│ │
│ <script> │
│ var x = 1; → window.x = 1 │
│ </script> │
│ <script> │
│ console.log(x); → 1 │
│ </script> │
│ │
│ Shared global scope │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ ES modules │
│ │
│ a.js │
│ const x = 1; │
│ │
│ b.js │
│ console.log(x); → ReferenceError │
│ │
│ Each module isolated │
│ │
└──────────────────────────────────────────────┘
b – Export and import syntax
ES Modules have several forms of export and import. This section covers them all.
Named exports:
Export individual values with names:
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
Or export at the end:
// math.js
const PI = 3.14159;
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
export { PI, add, subtract };
Default export:
Each module can have one default export:
// greet.js
export default function greet(name) {
return `Hello, ${name}`;
}
Default exports can be any value — function, class, object, primitive:
export default 42;
export default 'hello';
export default { a: 1 };
export default class Person {}
Mixing default and named exports:
// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';
export const VERSION = '1.0';
Importing named exports:
// main.js
import { add, subtract, PI } from './math.js';
console.log(add(1, 2));
console.log(PI);
Importing default exports:
// main.js
import greet from './greet.js';
console.log(greet('Alice'));
You can name the default whatever you want — the name isn’t fixed.
Importing both:
// main.js
import main, { helper, VERSION } from './utils.js';
console.log(main());
console.log(helper());
console.log(VERSION);
The default comes first, named imports in braces.
Renaming imports:
import { add as sum, PI as pi } from './math.js';
console.log(sum(1, 2));
console.log(pi);
Renaming exports:
// math.js
const add = (a, b) => a + b;
export { add as sum };
Namespace import:
Import everything as an object:
import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.PI);
console.log(math.subtract(5, 3));
If there’s also a default export, it’s on .default:
import * as utils from './utils.js';
console.log(utils.default());
console.log(utils.helper());
console.log(utils.VERSION);
Side-effect only import:
Import a module for its side effects — no bindings:
import './styles.css';
import './polyfill.js';
The module runs but nothing is imported.
Re-exporting:
Forward exports from another module:
// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';
This is common for barrel files — a single entry point for a folder.
Re-export everything:
// index.js
export * from './math.js';
export * from './string-utils.js';
Note: export * does not re-export defaults.
Import all + re-export:
// index.js
export * from './math.js';
export { default } from './main-utils.js';
All import/export forms:
| Form | Meaning |
|---|---|
import x from './m.js' | Default import |
import { a, b } from './m.js' | Named imports |
import { a as x } from './m.js' | Rename |
import x, { a } from './m.js' | Default + named |
import * as m from './m.js' | Namespace |
import './m.js' | Side effect only |
export const x = 1 | Inline named export |
export { x } | Export at end |
export { x as y } | Rename export |
export default x | Default export |
export { x } from './m.js' | Re-export |
export * from './m.js' | Re-export all |
export * as m from './m.js' | Re-export as namespace |
Import paths:
Relative paths must start with ./ or ../:
import { add } from './math.js'; // ✅ same folder
import { add } from '../utils/math.js'; // ✅ parent folder
import { add } from 'math.js'; // ❌ looks in node_modules
Node.js and bundlers resolve bare imports from node_modules:
import React from 'react';
import { useState } from 'react';
Browsers can’t do bare imports without an import map:
<script type="importmap">
{
"imports": {
"react": "/node_modules/react/index.js"
}
}
</script>
File extensions:
In browsers, extensions are required:
import { add } from './math.js'; // ✅
import { add } from './math'; // ❌ browser fails
Node.js requires the extension for relative imports (unless using a bundler).
A complete example:
// string-utils.js
export const upper = s => s.toUpperCase();
export const lower = s => s.toLowerCase();
export default function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
// index.js — barrel file
export * from './string-utils.js';
export * from './math.js';
export { default as capitalize } from './string-utils.js';
// app.js
import { add, multiply, upper, lower, capitalize } from './index.js';
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12
console.log(upper('hello')); // HELLO
console.log(capitalize('hi')); // Hi
A note on live bindings:
ES Module imports are live bindings — they reflect changes to the exported value:
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 ← live binding
You can’t reassign an imported binding, but you can see changes if the exporter updates it.
c – Dynamic imports and module patterns
Static imports are resolved before the module runs. Dynamic imports load modules at runtime — perfect for code splitting, conditional loading, and lazy evaluation.
Dynamic import():
import() is a function that returns a Promise resolving to the module namespace:
const module = await import('./math.js');
console.log(module.add(1, 2));
// Or with .then()
import('./math.js').then(m => {
console.log(m.add(1, 2));
});
Key differences from static import:
| Aspect | import | import() |
|---|---|---|
| When resolved | Load time | Runtime |
| Returns | Module binding | Promise |
| Where allowed | Top level | Anywhere |
| Named bindings | Direct | On module object |
| Conditional | ❌ | ✅ |
Conditional loading:
if (user.isAdmin) {
const { adminPanel } = await import('./admin.js');
adminPanel.open();
}
Lazy loading on demand:
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
The modal code is only downloaded when the user clicks.
Code splitting in bundlers:
const { default: HeavyComponent } = await import('./HeavyComponent.js');
render(HeavyComponent);
Bundlers like Vite and Webpack create a separate chunk for HeavyComponent.js, loaded only when needed.
Error handling with dynamic imports:
try {
const module = await import('./maybe-missing.js');
module.run();
} catch (err) {
console.error('Failed to load module:', err.message);
}
If the module fails to load (network error, syntax error), the Promise rejects.
Dynamic import with named exports:
const { add, subtract } = await import('./math.js');
console.log(add(1, 2));
console.log(subtract(5, 3));
Destructure named exports directly from the module object.
Dynamic import with default:
const { default: greet } = await import('./greet.js');
console.log(greet('Alice'));
You need to rename default because it’s a reserved word.
Import maps for browser module resolution:
<script type="importmap">
{
"imports": {
"lodash": "/vendor/lodash-es.js",
"utils/": "/js/utils/"
}
}
</script>
<script type="module">
import { chunk } from 'lodash';
import { add } from 'utils/math.js';
</script>
Import maps let you use bare specifiers in browsers.
Module patterns:
Pattern 1 — Barrel file (index.js):
// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';
// consumer
import { Button, Input, Modal } from './components/index.js';
Consumers import from one entry point, not individual files.
Pattern 2 — Facade module:
// api/index.js
export { get as getUser } from './user.js';
export { get as getOrder } from './order.js';
export { post } from './http.js';
Pattern 3 — Conditional exports in package.json:
{
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
}
Pattern 4 — Circular dependency awareness:
Circular imports are possible but tricky. Keep modules acyclic when you can.
// a.js
import { b } from './b.js';
export const a = () => b();
// b.js
import { a } from './a.js';
export const b = () => 'b';
The above works because b doesn’t call a at module load time — only inside the function.
Pattern 5 — Lazy module initialization:
let instance = null;
export async function getInstance() {
if (!instance) {
const { Heavy } = await import('./heavy.js');
instance = new Heavy();
}
return instance;
}
Pattern 6 — Polyfills via side effect:
// main.js
import './polyfills.js';
import { run } from './app.js';
run();
polyfills.js runs when imported but exports nothing.
Pattern 7 — Environment-specific modules:
const config = await import(`./config.${process.env.NODE_ENV}.js`);
export default config.default;
Pattern 8 — Route-based code splitting:
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/contact': () => import('./pages/Contact.js')
};
async function navigate(path) {
const { default: Page } = await routes[path]();
render(Page);
}
Pattern 9 — Feature flags:
if (featureFlags.newDashboard) {
const { Dashboard } = await import('./new-dashboard.js');
render(Dashboard);
} else {
const { Dashboard } = await import('./old-dashboard.js');
render(Dashboard);
}
Pattern 10 — Testing with dynamic imports:
const { sum } = await import('./utils.js');
test('sum adds numbers', () => {
expect(sum(1, 2)).toBe(3);
});
Module loading order:
┌──────────────────────────────────────────────┐
│ Static imports │
│ │
│ main.js │
│ import { a } from './a.js'; │
│ import { b } from './b.js'; │
│ │
│ 1. Fetch a.js │
│ 2. Fetch b.js │
│ 3. Evaluate a.js │
│ 4. Evaluate b.js │
│ 5. Evaluate main.js │
│ │
│ All modules loaded before main.js runs │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Dynamic imports │
│ │
│ main.js │
│ button.onclick = async () => { │
│ const m = await import('./modal.js'); │
│ }; │
│ │
│ 1. main.js runs │
│ 2. User clicks button │
│ 3. Fetch modal.js │
│ 4. Evaluate modal.js │
│ 5. Result returned │
│ │
│ Loaded on demand │
│ │
└──────────────────────────────────────────────┘
Complete Example Session
// ============================================
// PART 1: NAMED EXPORTS
// ============================================
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// main.js
import { add, subtract, PI } from './math.js';
console.log(add(1, 2));
// [ 3 ]
console.log(PI);
// [ 3.14159 ]
// ============================================
// PART 2: DEFAULT EXPORT
// ============================================
// greet.js
export default function greet(name) {
return `Hello, ${name}`;
}
// main.js
import greet from './greet.js';
console.log(greet('Alice'));
// [ Hello, Alice ]
// ============================================
// PART 3: MIXED EXPORTS
// ============================================
// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';
export const VERSION = '1.0';
// main.js
import main, { helper, VERSION } from './utils.js';
console.log(main());
// [ main ]
console.log(helper());
// [ helper ]
console.log(VERSION);
// [ 1.0 ]
// ============================================
// PART 4: RENAMING
// ============================================
// main.js
import { add as sum } from './math.js';
console.log(sum(1, 2));
// [ 3 ]
// ============================================
// PART 5: NAMESPACE IMPORT
// ============================================
// main.js
import * as math from './math.js';
console.log(math.add(1, 2));
// [ 3 ]
console.log(math.PI);
// [ 3.14159 ]
// ============================================
// PART 6: SIDE EFFECT ONLY
// ============================================
// main.js
import './polyfill.js';
// polyfill.js runs but nothing imported
// ============================================
// PART 7: RE-EXPORT
// ============================================
// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';
// main.js
import { add, greet } from './index.js';
console.log(add(1, 2));
// [ 3 ]
console.log(greet('Alice'));
// [ Hello, Alice ]
// ============================================
// PART 8: EXPORT ALL
// ============================================
// index.js
export * from './math.js';
export * from './string-utils.js';
// main.js
import { add, upper } from './index.js';
// ============================================
// PART 9: DYNAMIC IMPORT
// ============================================
async function load() {
const module = await import('./math.js');
console.log(module.add(1, 2));
}
load();
// [ 3 ]
// ============================================
// PART 10: CONDITIONAL DYNAMIC
// ============================================
async function init(user) {
if (user.isAdmin) {
const { adminPanel } = await import('./admin.js');
adminPanel.open();
}
}
// ============================================
// PART 11: LAZY LOADING
// ============================================
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#show-modal');
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
});
// ============================================
// PART 12: DESTRUCTURING DYNAMIC IMPORTS
// ============================================
async function run() {
const { add, subtract } = await import('./math.js');
console.log(add(1, 2));
console.log(subtract(5, 3));
}
run();
// [ 3 ]
// [ 2 ]
// ============================================
// PART 13: DYNAMIC DEFAULT
// ============================================
async function greet() {
const { default: greetFn } = await import('./greet.js');
console.log(greetFn('Alice'));
}
greet();
// [ Hello, Alice ]
// ============================================
// PART 14: LIVE BINDINGS
// ============================================
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count);
// [ 0 ]
increment();
console.log(count);
// [ 1 ]
// ============================================
// PART 15: MODULE SINGLETON
// ============================================
// counter.js
let count = 0;
export function increment() { return ++count; }
// a.js
import { increment } from './counter.js';
increment();
// [ 1 ]
// b.js
import { increment } from './counter.js';
increment();
// [ 2 ]
// ============================================
// PART 16: BARREL FILE
// ============================================
// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
// app.js
import { Button, Input } from './components/index.js';
// ============================================
// PART 17: DYNAMIC WITH ERROR HANDLING
// ============================================
async function safeImport(path) {
try {
return await import(path);
} catch (err) {
console.error('Failed:', err.message);
return null;
}
}
// ============================================
// PART 18: ROUTE-BASED SPLITTING
// ============================================
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js')
};
async function navigate(path) {
const { default: Page } = await routes[path]();
render(Page);
}
// ============================================
// PART 19: ENVIRONMENT-SPECIFIC
// ============================================
// const config = await import(`./config.${process.env.NODE_ENV}.js`);
// export default config.default;
// ============================================
// PART 20: FULL SCRIPT
// ============================================
// math44.js
export function add44(a, b) {
return a + b;
}
export const PI44 = 3.14159;
export default function multiply44(a, b) {
return a * b;
}
// main44.js
import multiply44, { add44, PI44 } from './math44.js';
console.log(add44(2, 3));
console.log(PI44);
console.log(multiply44(2, 3));
import { add44 as sum44 } from './math44.js';
console.log(sum44(1, 2));
import * as math44 from './math44.js';
console.log(math44.add44(1, 2));
console.log(math44.PI44);
console.log(math44.default(2, 3));
const module44 = await import('./math44.js');
console.log(module44.add44(1, 2));
Quick Reference
Export Forms
| Form | Example |
|---|---|
| Named export | export const x = 1; |
| Named export (end) | export { x, y }; |
| Rename | export { x as y }; |
| Default | export default function() {} |
| Re-export | export { x } from './m.js'; |
| Re-export all | export * from './m.js'; |
| Namespace re-export | export * as ns from './m.js'; |
Import Forms
| Form | Example |
|---|---|
| Named | import { x } from './m.js'; |
| Rename | import { x as y } from './m.js'; |
| Default | import x from './m.js'; |
| Default + named | import x, { y } from './m.js'; |
| Namespace | import * as m from './m.js'; |
| Side-effect | import './m.js'; |
| Dynamic | await import('./m.js') |
Export Types
| Type | Count | Import style |
|---|---|---|
| Named | Many | import { x } |
| Default | One | import x |
| Both | 1 + many | import x, { y } |
ESM vs CommonJS
| Aspect | ESM | CommonJS |
|---|---|---|
| Syntax | import/export | require/module.exports |
| Loading | Static | Dynamic |
| Async | Yes | No |
this at top | undefined | module.exports |
| Strict mode | Always | Optional |
| Tree shaking | Yes | No |
| Browser | Native | Bundler |
Static vs Dynamic
| Aspect | import | import() |
|---|---|---|
| Timing | Load | Runtime |
| Returns | Binding | Promise |
| Condition | ❌ | ✅ |
| Location | Top level | Anywhere |
| Splitting | No | Yes |
Paths
| Path | Meaning |
|---|---|
./m.js | Same folder |
../m.js | Parent folder |
/m.js | Absolute (web root) |
pkg | node_modules (Node/bundler) |
https://... | Remote (browser) |
Key Rules
| Rule | Details |
|---|---|
| Module scope | Variables not global |
| Singleton | Evaluated once |
| Deferred | Runs after HTML parsed |
| Strict mode | Always |
Top-level this | undefined |
| Extensions | Required in browser |
| Relative paths | Must start with ./ or ../ |
Best Practices
✅ Do This:
// Use named exports for most things
export function add(a, b) { return a + b; } // ✅
// Use default for the main thing a module provides
export default class User {} // ✅
// Use explicit extensions in browser code
import { x } from './m.js'; // ✅
// Use barrel files for folders
export * from './Button.js'; // ✅
// Use dynamic imports for code splitting
const m = await import('./heavy.js'); // ✅
// Prefer static imports when possible
import { x } from './m.js'; // ✅
// Use namespaces to avoid conflicts
import * as math from './math.js'; // ✅
// Re-export to create clean public APIs
export { add } from './math.js'; // ✅
// Use dynamic imports with try/catch
try { await import('./m.js'); } catch (e) {} // ✅
❌ Don’t Do This:
// Don't use extensionless paths in browsers
import { x } from './m'; // ❌ browser fails
// Don't use bare paths without import maps
import { x } from 'math'; // ❌ browser fails
import { x } from 'math'; // ✅ in Node/bundler
// Don't mix require and import
const x = require('./m'); // ❌ in ESM
import { x } from './m.js'; // ✅
// Don't use module.exports in ESM
module.exports = { x }; // ❌ in ESM
// Don't reassign imported bindings
import { x } from './m.js';
x = 10; // ❌ TypeError
// Don't rely on load order for side effects
import './a.js'; // ⚠️ race conditions
// Don't create circular dependencies
// a.js imports b.js, b.js imports a.js // ⚠️ fragile
// Don't use dynamic imports for everything
const m = await import('./small.js'); // ⚠️ overhead
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing ./ | Bare import fails | Add ./ |
| Missing extension | Browser fails | Add .js |
| Mixing require/import | Error | Use ESM throughout |
| Reassigning imports | TypeError | Use getters/exports |
| Circular deps | Undefined values | Refactor |
this at top | undefined | Don’t rely on it |
__dirname missing | ReferenceError | Use import.meta.url |
| Dynamic import path | Not a literal | Use static or import.meta |
import.meta in ESM:
console.log(import.meta.url);
// [ file:///path/to/module.js ]
// Get directory
const dir = new URL('.', import.meta.url).pathname;
Replaces __dirname in CommonJS.
Real-World Examples
1. Named Exports
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
// main.js
import { add, PI } from './math.js';
console.log(add(1, 2));
// [ 3 ]
2. Default Export
// greet.js
export default function greet(name) {
return `Hello, ${name}`;
}
// main.js
import greet from './greet.js';
console.log(greet('Alice'));
// [ Hello, Alice ]
3. Mixed Exports
// utils.js
export default function main() { return 'main'; }
export const helper = () => 'helper';
// main.js
import main, { helper } from './utils.js';
4. Renaming
import { add as sum } from './math.js';
console.log(sum(1, 2));
// [ 3 ]
5. Namespace Import
import * as math from './math.js';
console.log(math.add(1, 2));
console.log(math.PI);
6. Side-Effect Import
import './polyfill.js';
7. Re-Export
// index.js
export { add, subtract } from './math.js';
export { default as greet } from './greet.js';
8. Export All
// index.js
export * from './math.js';
export * from './string-utils.js';
9. Dynamic Import
const module = await import('./math.js');
console.log(module.add(1, 2));
// [ 3 ]
10. Conditional Dynamic Import
if (user.isAdmin) {
const { adminPanel } = await import('./admin.js');
adminPanel.open();
}
11. Lazy Loading
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
12. Destructured Dynamic Import
const { add, subtract } = await import('./math.js');
13. Dynamic Default Import
const { default: greet } = await import('./greet.js');
console.log(greet('Alice'));
14. Live Bindings
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
increment();
console.log(count);
// [ 1 ]
15. Module Singleton
// singleton.js
let instance;
export function getInstance() {
return instance ||= {};
}
16. Barrel File
// components/index.js
export { Button } from './Button.js';
export { Input } from './Input.js';
17. Dynamic with Error Handling
try {
const m = await import('./maybe.js');
} catch (err) {
console.error('Failed:', err.message);
}
18. Route-Based Splitting
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js')
};
19. Environment Config
const config = await import(`./config.${process.env.NODE_ENV}.js`);
export default config.default;
20. Full Script
// math44.js
export function add44(a, b) {
return a + b;
}
export const PI44 = 3.14159;
export default function multiply44(a, b) {
return a * b;
}
// main44.js
import multiply44, { add44, PI44 } from './math44.js';
console.log(add44(2, 3));
console.log(PI44);
console.log(multiply44(2, 3));
import { add44 as sum44 } from './math44.js';
console.log(sum44(1, 2));
import * as math44 from './math44.js';
console.log(math44.add44(1, 2));
console.log(math44.PI44);
console.log(math44.default(2, 3));
const module44 = await import('./math44.js');
console.log(module44.add44(1, 2));
Visual: Module Loading
┌──────────────────────────────────────────────┐
│ main.js │
│ import { a } from './a.js' │
│ import { b } from './b.js' │
│ │
│ a.js │
│ import { c } from './c.js' │
│ │
│ Dependency graph: │
│ │
│ main.js │
│ ├── a.js │
│ │ └── c.js │
│ └── b.js │
│ │
│ Evaluation order (depth-first): │
│ c.js → a.js → b.js → main.js │
│ │
└──────────────────────────────────────────────┘
Visual: Static vs Dynamic
┌──────────────────────────────────────────────┐
│ Static import │
│ │
│ import { x } from './m.js' │
│ │
│ • Resolved before code runs │
│ • Named bindings available │
│ • Can be tree-shaken │
│ • Blocking (in dependency order) │
│ • Top level only │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Dynamic import │
│ │
│ const m = await import('./m.js') │
│ │
│ • Resolved at runtime │
│ • Returns a Promise │
│ • Always loads whole module │
│ • Non-blocking │
│ • Anywhere in code │
│ • Enables code splitting │
│ │
└──────────────────────────────────────────────┘
Visual: Export/Import Types
┌──────────────────────────────────────────────┐
│ math.js │
│ │
│ export const PI = 3.14; ← named │
│ export function add() {} ← named │
│ export default function() {} ← default │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ main.js │
│ │
│ import def from './math.js' │
│ → gets the default │
│ │
│ import { PI, add } from './math.js' │
│ → gets named │
│ │
│ import def, { PI } from './math.js' │
│ → gets both │
│ │
│ import * as math from './math.js' │
│ → namespace object │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Syntax | Example |
|---|---|---|
| Named export | export const x = 1 | export function add() {} |
| Default export | export default x | export default function() {} |
| Named import | import { x } from './m.js' | Get named binding |
| Default import | import x from './m.js' | Get default |
| Mixed | import x, { y } from './m.js' | Both |
| Rename | import { x as y } | Rename import |
| Namespace | import * as m | All as object |
| Side effect | import './m.js' | Just run |
| Re-export | export { x } from './m.js' | Forward |
| Export all | export * from './m.js' | Forward all |
| Dynamic | await import('./m.js') | Runtime load |
| Dynamic default | const { default: x } = await import(...) | Destructure |
Key takeaways:
- ES Modules are the standard for splitting JavaScript into files
- Use
exportto expose values;importto consume them - Named exports allow many per module; default allows one
- Namespace import (
import * as m) groups everything into an object - Re-exports and barrel files create clean public APIs
- Dynamic imports (
import()) load at runtime and return a Promise - Modules have their own scope — no global leakage
- Modules are singletons — evaluated once, shared everywhere
- Modules run in strict mode by default — no
'use strict'needed thisat the top level isundefinedimport.meta.urlreplaces__dirnamein ESM- File extensions are required for relative imports in browsers
- Use
./or../for relative paths — bare paths are fornode_modules
Remember: ES Modules are how modern JavaScript is organized. Export what you want to share, import what you need. Use named exports for flexibility, default for the main thing. Reach for dynamic import() when you need conditional loading or code splitting. Keep your modules focused and avoid circular dependencies. Master ESM, and your code becomes organized, tree-shakeable, and ready for the modern web.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!