**Why modules exist**
Before ES modules, every `<script>` tag shared a single global scope. Libraries had to namespace everything under one global variable (`jQuery`, `_`, `React`) to avoid collisions, and loading order mattered critically. Modules solve this by giving each file its own scope — nothing leaks to the global unless explicitly exported:
```js
// Without modules (old scripts): everything global, collision risk
var PI = 3.14; // collides if another script declares PI
function add(a, b) { } // pollutes window.add
// With modules: completely isolated
const PI = 3.14; // only visible inside this file
export function add(a, b) { return a + b; } // explicitly shared
```
**Named and default exports**
```js
// --- math.js ---
export function add(a, b) { return a + b; } // named
export function sub(a, b) { return a - b; } // named
export const PI = 3.14159; // named
export default class Calculator { // default (one per module)
add(a, b) { return a + b; }
}
```
**Importing**
```js
import { add, PI } from './math.js'; // named
import { add as sum } from './math.js'; // renamed
import * as math from './math.js'; // namespace object
import Calculator from './math.js'; // default (any name)
import Calculator, { add, PI } from './math.js'; // both
math.add(1, 2); // via namespace
math.PI; // 3.14159
```
**Re-exports — barrel files**
A barrel file (`index.js`) aggregates exports from multiple files into one entry point, so consumers import from one place instead of navigating file paths:
```js
// --- src/utils/index.js (barrel) ---
export { add, PI } from './math.js';
export { default as Calculator } from './math.js';
export * from './strings.js'; // all named exports from strings.js
// --- consumer ---
import { add, Calculator } from './src/utils'; // one clean import
```
**Dynamic `import()` — load on demand**
Static imports run at module load time. `import()` returns a Promise and lets you load a module only when actually needed — great for code-splitting:
```js
// Conditional loading
async function loadChart() {
if (!userWantsChart) return;
const { default: Chart } = await import('./chart.js');
return new Chart();
}
// Parallel loading — both start at the same time
const [{ default: A }, { default: B }] = await Promise.all([
import('./a.js'),
import('./b.js'),
]);
// Dynamic path (computed at runtime)
const locale = navigator.language.slice(0, 2); // 'uk', 'en', ...
const { messages } = await import(`./locales/${locale}.js`);
```
**Error handling with dynamic import**
```js
try {
const mod = await import('./heavy-plugin.js');
mod.init();
} catch (err) {
console.warn('Plugin failed to load, continuing without it', err);
}
```
**Top-level `await`**
Inside a module (not inside a function), you can `await` directly. The module graph waits for it to settle before any importer runs:
```js
// --- db.js ---
const connection = await connectToDatabase(config); // runs once at load
export { connection };
// --- main.js ---
import { connection } from './db.js'; // guaranteed to be ready
```
**`import.meta` — module metadata**
```js
import.meta.url; // full URL of this module file
import.meta.env; // Vite/bundler: environment variables
import.meta.env.DEV; // true in development
// Resolve a path relative to this module's location:
const dataPath = new URL('./data.json', import.meta.url).pathname;
```
**Module caching**
A module is evaluated only once, no matter how many times it's imported. All importers share the same module instance:
```js
// counter.js
let count = 0;
export const inc = () => ++count;
export const get = () => count;
// a.js
import { inc } from './counter.js';
inc(); inc();
// b.js
import { get } from './counter.js';
get(); // 2 — same instance, not a fresh copy
```
Module vs Script, live bindings, circular deps, tree-shaking
**Module vs Script**
| Feature | Script | Module |
|---|---|---|
| Scope | Global | Module-local |
| `this` at top level | `window` | `undefined` |
| Strict mode | opt-in | always on |
| `import`/`export` | not allowed | allowed |
| Execution | once per `<script>` tag | once, then cached |
| HTML | `<script>` | `<script type="module">` |
| Deferred | no (blocks HTML) | yes (deferred by default) |
**Import is a live binding — not a copy**
Unlike CommonJS (`require`), ES module imports are live bindings. When the exporting module mutates the value, importers see the update:
```js
// counter.js
export let count = 0;
export function inc() { count++; }
// main.js
import { count, inc } from './counter.js';
console.log(count); // 0
inc();
console.log(count); // 1 — live binding, not a snapshot
// CommonJS (Node.js require) would give 0 again — it copies the value
```
**Circular dependencies**
ES modules handle cycles by providing an incomplete binding at first — the value is `undefined` until the module finishes evaluating. This can cause subtle bugs:
```
BAD: a.js --> b.js --> a.js (cycle)
GOOD: a.js \
--> shared.js
b.js /
```
Rule: if you see a cycle in your bundler output, extract the shared code to a third file. Cycles are a design smell, not a bundler limitation.
**Tree-shaking**
Bundlers (Vite, webpack, Rollup) can remove unused exports from the final bundle because ES module structure is static and known at build time. This is called *tree-shaking*:
```js
// utils.js exports 10 functions
export function used() { ... }
export function neverImported() { ... } // removed by bundler
// main.js
import { used } from './utils.js'; // only 'used' ends up in bundle
```
Tree-shaking only works with named exports and static imports. Dynamic `import()` and `export default` (single object with many methods) prevent effective tree-shaking.
Write a module `math.js` that exports two named functions: `add(a, b)` and `multiply(a, b)`. In `main.js`, import both and use them.
// math.js
export function add(a, b) {
return a + b;
}
// complete multiply and export it
function multiply(a, b) {
return a * b;
}
// main.js
import { add, multiply } from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
Solution
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// main.js
import { add, multiply } from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
Create a module `greet.js` with a default export — a function `greet(name)` that returns `'Hello, <name>!'`. Import it in `main.js` under any name and call it.
// greet.js
// add default export here
function greet(name) {
return `Hello, ${name}!`;
}
// main.js
import greet from './greet.js';
console.log(greet('Alice')); // Hello, Alice!
// you can also import under a different name:
import sayHello from './greet.js';
console.log(sayHello('Bob')); // Hello, Bob!
Solution
// greet.js
export default function greet(name) {
return `Hello, ${name}!`;
}
// main.js
import greet from './greet.js';
console.log(greet('Alice')); // Hello, Alice!
import sayHello from './greet.js';
console.log(sayHello('Bob')); // Hello, Bob!
A module `utils.js` exports `formatDate` and `formatCurrency`. Import them both aliased: `formatDate` as `dateStr`, `formatCurrency` as `money`. Also import the entire module as a namespace object `utils` and use `utils.formatDate` and `utils.formatCurrency`.
// utils.js
export function formatDate(d) {
return d.toISOString().slice(0, 10);
}
export function formatCurrency(n) {
return '$' + n.toFixed(2);
}
// main.js - aliased imports:
import { formatDate as dateStr, formatCurrency as money } from './utils.js';
console.log(dateStr(new Date('2024-01-15'))); // 2024-01-15
console.log(money(9.9)); // $9.90
// namespace import:
import * as utils from './utils.js';
console.log(utils.formatDate(new Date('2024-01-15')));
console.log(utils.formatCurrency(9.9));
Solution
// utils.js
export function formatDate(d) {
return d.toISOString().slice(0, 10);
}
export function formatCurrency(n) {
return '$' + n.toFixed(2);
}
// main.js
import { formatDate as dateStr, formatCurrency as money } from './utils.js';
console.log(dateStr(new Date('2024-01-15'))); // 2024-01-15
console.log(money(9.9)); // $9.90
import * as utils from './utils.js';
console.log(utils.formatDate(new Date('2024-01-15')));
console.log(utils.formatCurrency(9.9));
You have two modules: `circle.js` (exports `area` and `perimeter`) and `rect.js` (exports `area` and `perimeter`). Create an `index.js` that re-exports everything from both, renaming to avoid name collisions. Then import from `index.js` only.
// circle.js
export const area = r => Math.PI * r * r;
export const perimeter = r => 2 * Math.PI * r;
// rect.js
export const area = (w, h) => w * h;
export const perimeter = (w, h) => 2 * (w + h);
// index.js — re-export with renames:
export { area as circleArea, perimeter as circlePerimeter } from './circle.js';
// add rect re-exports here
// main.js
import { circleArea, rectArea } from './index.js';
console.log(circleArea(5).toFixed(2)); // 78.54
console.log(rectArea(3, 4)); // 12
Solution
// circle.js
export const area = r => Math.PI * r * r;
export const perimeter = r => 2 * Math.PI * r;
// rect.js
export const area = (w, h) => w * h;
export const perimeter = (w, h) => 2 * (w + h);
// index.js
export { area as circleArea, perimeter as circlePerimeter } from './circle.js';
export { area as rectArea, perimeter as rectPerimeter } from './rect.js';
// main.js
import { circleArea, rectArea } from './index.js';
console.log(circleArea(5).toFixed(2)); // 78.54
console.log(rectArea(3, 4)); // 12
Use `import()` to load a module conditionally. If `useLargeLib` is `true`, dynamically import `heavy.js` (which exports a default `process` function). Otherwise skip the load. Show how to use the loaded module.
// heavy.js
export default function process(data) {
return data.toUpperCase();
}
// main.js
async function run(useLargeLib) {
if (useLargeLib) {
// dynamically import heavy.js here
// call the default export with 'hello'
} else {
console.log('Skipped heavy module');
}
}
run(true);
Solution
// heavy.js
export default function process(data) {
return data.toUpperCase();
}
// main.js
async function run(useLargeLib) {
if (useLargeLib) {
const { default: process } = await import('./heavy.js');
console.log(process('hello')); // HELLO
} else {
console.log('Skipped heavy module');
}
}
run(true);
Create a `config.js` module that exports a single mutable `config` object. Show that importing it in two places gives the same object reference — mutations in one place are visible in the other.
`a.js` imports from `b.js`, and `b.js` imports from `a.js` — circular. Show what happens: `a.js` exports `A = 'A'`, imports `B` from `b.js`. `b.js` exports `B = 'B'`, imports `A` from `a.js`. In `main.js`, import both and log them. Then rewrite to break the cycle using a shared `constants.js`.
// --- CIRCULAR (problematic) ---
// a.js
import { B } from './b.js';
export const A = 'A';
console.log('In a.js, B =', B); // may be undefined!
// b.js
import { A } from './a.js';
export const B = 'B';
console.log('In b.js, A =', A); // may be undefined!
// --- FIX: break the cycle ---
// constants.js
// export A and B from here, no imports
Solution
// CIRCULAR — a.js imports b, b imports a
// When a.js is first evaluated, b.js is not yet done,
// so B may be undefined when a.js runs its top-level code.
// --- FIX ---
// constants.js
export const A = 'A';
export const B = 'B';
// a.js
import { B } from './constants.js'; // no cycle
console.log('In a.js, B =', B); // 'B' (always defined)
// b.js
import { A } from './constants.js'; // no cycle
console.log('In b.js, A =', A); // 'A' (always defined)
Write an async function `loadPlugin(name)` that dynamically imports `./plugins/${name}.js` and calls its default-exported `init()` function. If the import fails (e.g. module not found), catch the error and log `'Plugin not found: <name>'`.
async function loadPlugin(name) {
try {
// dynamically import the plugin
// call the init() function
} catch (err) {
console.log(`Plugin not found: ${name}`);
}
}
await loadPlugin('analytics');
await loadPlugin('nonexistent'); // Plugin not found: nonexistent
In a module `data.js`, use top-level `await` to fetch data before exporting it. Simulate this with a `delay` function. Export the resolved `data` const. In `main.js`, import and use it — no extra await needed.
// data.js
const delay = ms => new Promise(r => setTimeout(r, ms));
await delay(10); // top-level await
export const data = { loaded: true, items: [1, 2, 3] };
// main.js
import { data } from './data.js';
// by the time this runs, data is already resolved
console.log(data.loaded); // true
console.log(data.items); // [1, 2, 3]
Solution
// data.js
const delay = ms => new Promise(r => setTimeout(r, ms));
await delay(10);
export const data = { loaded: true, items: [1, 2, 3] };
// main.js
import { data } from './data.js';
console.log(data.loaded); // true
console.log(data.items); // [1, 2, 3]
Demonstrate three key differences between ES modules and classic scripts: 1. Module-scoped `this` is `undefined` at top level. 2. Modules have their own scope — variables are not global. 3. `import` declarations are hoisted (you can import at top, use at bottom). Show each with a comment explaining what the output would be.
// module.js (as a module — type='module' in HTML or .mjs in Node)
// 1. this at top level
console.log(typeof this); // 'undefined' (in module)
// 'object' (window) in classic script
// 2. module scope — x is NOT global
const x = 42;
console.log(typeof globalThis.x); // 'undefined' in module
// '42' if classic script
// 3. imports are hoisted — you can use them before their textual position
console.log(typeof add); // 'function' (import is hoisted)
import { add } from './math.js';
Solution
// module.js
// 1. this at top level in a module is undefined
console.log(typeof this); // 'undefined'
// 2. module-scoped const — not attached to globalThis
const x = 42;
console.log(typeof globalThis.x); // 'undefined'
// 3. import declarations are hoisted — safe to reference before textual position
console.log(typeof add); // 'function'
import { add } from './math.js';
No split tab
Cookie preferences
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.