Chapter 23 of 28 · JavaScript for Beginners — Complete Free Tutorial
JavaScript Modules (ES Modules)
Real applications span dozens of files. ES modules (the standard since 2015, usable natively in browsers) let you split code into files with explicit imports and exports — no globals, no load-order puzzles, real dependency graphs. This chapter covers the syntax, patterns and pitfalls.
Why modules?
- Namespaces — module scope replaces accidental globals; two files can both have
helper(). - Explicit dependencies — a file states exactly what it uses.
- Maintainability — small, testable files instead of one 3,000-line script.
- Reuse — one utility file, imported by many features.
- Lazy loading — dynamic
import()loads code only when needed.
Setup: type="module"
<script type="module" src="/js/app.js"></script>
Modules get, for free:
- Deferred execution — runs after parsing (like
defer). - Strict mode — sloppy-mode accidents (
x = 5creating globals) throw instead. - Isolated scope — nothing leaks to
window. - CORS — module files are fetched with CORS rules (serve from the same origin or with proper headers).
Exporting
// utils.js
export const PI = 3.14159;
export function circleArea(r) {
return PI * r * r;
}
export default function slugify(text) { // one default export per file
return text.toLowerCase().replace(/\s+/g, '-');
}
// also valid: named exports at the bottom
const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
export { clamp };
Importing
// app.js
import slugify from './utils.js'; // default import (any name)
import { PI, circleArea } from './utils.js'; // named imports (exact names)
import { clamp as clampNum } from './utils.js'; // rename
import * as utils from './utils.js'; // namespace object
console.log(utils.PI, circleArea(2), slugify('Hello World'));
// → 3.14159 12.56636 hello-world
| Export | Import |
|---|---|
export default fn | import fn from './x.js' (name is your choice) |
export const a / export { a, b } | import { a, b } from './x.js' (names must match) |
| multiple named exports | import * as everything from './x.js' |
Default vs named — choosing
- Default — one primary thing per file: a component, the main class, a single function.
- Named — toolkits, constants, multiple helpers (preferred in utilities — imports stay self-documenting).
- Mixing is allowed, but most style guides lean named-first.
Relative paths are mandatory
import x from './helper.js'; // ✅ same directory (note the ./)
import y from '/js/helper.js'; // ✅ absolute from the site root
import z from 'helper.js'; // ❌ treated as a bare specifier (needs a bundler)
Extensions are required for browser-native modules (.js); bundlers like Vite may resolve extensionless imports for you.
Organising a real project
js/
├─ app.js ← entry: wires everything together
├─ api.js ← fetch helpers
├─ state.js ← shared data
├─ ui/
│ ├─ header.js
│ └─ productList.js
└─ utils/
├─ format.js
└─ debounce.js
// app.js — the entry point
import { fetchProducts } from './api.js';
import { renderList } from './ui/productList.js';
import { debounce } from './utils/debounce.js';
async function init() {
const items = await fetchProducts();
renderList(items);
}
init();
Side-effect and re-export patterns
export { formatDate } from './date.js'; // re-export
export { default as Chart } from './chart.js'; // re-export default under a name
import './analytics.js'; // run for its side effect only
Dynamic import: load on demand
settingsBtn.addEventListener('click', async () => {
const { openSettings } = await import('./ui/settings.js');
openSettings(); // the code downloads only when the user opens settings
});
Dynamic imports return promises — perfect for code-splitting heavy features (charts, editors) so first load stays fast.
Circular imports
A imports B, B imports A — legal, but values may be partially initialised when accessed. Fix by breaking the cycle (move shared code to a third module). Treat cycles as a design smell.
Common mistakes
- Forgetting
type="module"— a plain script usingimportthrowsSyntaxError. - Missing
./or the.jsextension — the classic module 404. - Expecting module variables on
window— they are deliberately private; export them instead. - Named vs default mix-ups —
import { default as x }andimport x fromboth fetch the default; named braces must match export names exactly. - Module load order — modules are evaluated in dependency order automatically; no more hand-managed script tags.
- Importing everything into the entry file — pulls the whole graph; dynamic
import()for optional features.
Practice checklist
- Split a script into
utils.js(named + default exports) andapp.js; load withtype="module". - Verify isolation: assign a variable without declaring it — confirm strict mode throws.
- Lazy-load a secondary feature with dynamic
import()and watch the Network tab. - Create a barrel file (
index.js) re-exporting three utility modules.
Modules organise functions — classes organise domain models. Next: Classes & Object-Oriented JavaScript.
Frequently Asked Questions
JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.
They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.
Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.
Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.
The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.
== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.
JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.
A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.
Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.
Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.