skip to content

Modules and Bundling

How JavaScript source is split across files and linked back together — ES modules, CommonJS, and what the two do differently at load time. Children cover ESM semantics, dynamic import and code splitting, ESM/CJS interop, circular dependencies, and side effects with tree shaking.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In a CommonJS module, when you write `const cfg = require('./config')`, at what point does the code inside config.js actually run, and what value does require() hand back?

level: juniorimportance: must knowfreq 68%

answer

  1. a call, not a declaration
  2. blocking, nothing interleaves
  3. body runs to completion first
  4. you get module.exports, by reference
  5. seeded as an empty object

basics

~10 s

require() runs the target module's body synchronously at the call site and hands back whatever value module.exports holds when that body finishes. The calling file is blocked until the required module completes.

solid answer

~40 s

`require('./config')` is an ordinary function call, not a declaration, so it does its work exactly where it appears. It runs config.js's body to completion synchronously — the calling module is blocked meanwhile, so any logging, config parsing or table building at the top level of config.js happens right at that line. When the body finishes, `require` returns the current value of that module's `module.exports`, which is seeded as an empty object `{}` and is whatever the module attached properties to or replaced it with. The caller receives that object by reference, not a copy: it holds the very same object the module holds. If the module body throws, the exception propagates straight out of the `require()` call like any other synchronous error, so a `try/catch` around `require` catches it.

go deeper

for a junior

Be able to say plainly that require() runs the other file's code right there and gives you back that file's module.exports. Know that it is synchronous and returns a plain value, never a promise.

for a middle

Explain the mechanics: module.exports starts as an empty object, the body runs to completion, and the value in module.exports at that moment is what the caller receives — by reference, so both sides hold one object.

for a senior

Show that you reason about what runs at load time. Discuss depth-first nested loading, errors propagating synchronously out of the require call, and why a module that swaps module.exports after loading breaks callers who already hold the old object.

for a principal

Own the design implication: because loading is blocking and ordering is call-order, module bodies are a shared startup budget and an implicit initialisation contract. Set conventions about what may run at load time before the codebase accumulates hidden startup coupling.

## require is a function, not a declaration CommonJS is the module system Node adopted before ECMAScript had one of its own; it is not part of the ECMAScript language specification. Inside a CommonJS file you get three names to work with: `require`, `module`, and `exports`. The most important consequence of `require` being a plain function is that it has no special syntactic position and no separate pre-pass. It can sit at the top of a file, inside an `if`, inside a function body, or in the middle of an expression, and it does its work at the moment control reaches it. ```js // main.js console.log('before'); const cfg = require('./config'); // config.js runs here, right now console.log('after', cfg.port); ``` That is already a meaningful difference from a declarative import form, where the loading is part of the module's static structure rather than a statement that executes in order. ## The module object and its exports Before a module's body runs, the host creates a `module` object for that file whose `exports` property is initialised to a fresh empty object, and a local variable `exports` initialised to that same object. The body then either attaches properties to it or replaces `module.exports` outright. When the body finishes, the value currently stored in `module.exports` is exactly what `require` returns to the caller. That is the whole contract: *whatever `module.exports` points at when the body ends*. A file that never touches its exports still returns something — the empty object it was seeded with: ```js // side-effect-only.js globalThis.installed = true; // main.js const m = require('./side-effect-only'); console.log(m); // {} — the seeded object, not undefined ``` That is why `require('./x').doThing()` on a module that forgot to export throws `TypeError: ... is not a function` rather than a "module not found" style error: you got an object, just an empty one. ## Synchronous, run-to-completion loading The load is synchronous and runs the module body to completion. Nothing else in the program interleaves with it. If the required module itself requires other modules, those calls happen inside its body, so loading proceeds depth-first, and each nested body finishes before the `require` that triggered it returns. ```js // a.js console.log('a start'); require('./b'); console.log('a end'); // b.js console.log('b'); ``` Requiring `a.js` prints `a start`, `b`, `a end`. The ordering is entirely explained by ordinary synchronous function-call semantics — there is no queue, no callback, no promise involved. This is also why you cannot `await require(...)` meaningfully: `require` does not return a promise, so awaiting it just wraps an already-available value. ## Returned by reference, not copied The caller and the module end up holding the same object. If the module later mutates a property on the object it exported, holders see the change, because there is one object. What does *not* propagate is a later reassignment of `module.exports` itself: a caller that already captured the old object keeps pointing at the old object, since it holds a reference to a value, not a live view of the module's `module.exports` slot. ```js // late.js module.exports = { ready: false }; setTimeout(() => { module.exports = { ready: true }; }, 0); // a caller that already required this file still sees { ready: false } ``` The practical rule that falls out: if a module needs to publish something that changes over time, mutate a property on the exported object or expose a function, rather than swapping the exports object out from under everyone. ## When loading throws A module body is just code, so it can throw. The exception travels out of the `require()` call to the requiring module's stack frame, and can be caught: ```js try { const plugin = require('./optional-plugin'); } catch (err) { // the module body threw, or the file could not be loaded } ``` Because the failure is synchronous, there is no rejected promise and no unhandled-rejection warning involved. ## Interview traps to be ready for The common wrong answers are that `require` returns a promise, that it schedules loading in the background, or that the returned value is some special namespace wrapper. All three are the same misunderstanding: `require` is a normal, blocking function call that returns an ordinary JavaScript value. The second trap is expecting `require` to hoist. It does not; a `require` placed inside a function body simply does not run until that function is called, which is the entire basis of the lazy-require idiom.

  • If a file never assigns to module.exports at all, what does require() of it return?
    An empty object. Each module's `module.exports` is seeded with a fresh `{}` before the body runs, so a file that only does side effects still hands back that object. That is why calling a missing export throws `TypeError: x is not a function` rather than something about a missing module — you got an object, it just has no properties.
  • Does a require() call have to appear at the top of a file?
    No. It is an ordinary function call, so it can be conditional or sit inside a function body, and it does nothing until control reaches it. Deferring a require into the function that needs it moves the module's load-time work off the startup path. The trade-off is that the load cost, and any error it throws, now shows up at call time instead of at startup.
  • What happens if the required module's body throws while it is loading?
    The exception propagates synchronously out of the `require()` call into the requiring module, exactly like an error thrown by any function you call. A surrounding `try/catch` catches it, and the assignment target never receives a value. Nothing async is involved, so there is no rejected promise and no unhandled-rejection path to worry about.

saying these in an interview costs you the question

  • Says require() returns a promise you should await
  • Thinks require() loads the file asynchronously in the background
  • Believes require() hoists to the top like a declaration
  • Assumes a module with no exports returns undefined
  • Thinks the caller gets a deep copy of the exported object

context

open as a page

In JavaScript, what does the expression `import('./math.js')` evaluate to, and how does that differ from a static `import ... from './math.js'` declaration?

level: juniorimportance: must knowfreq 72%

basics

~20 s

import() is a syntactic form that starts loading a module at runtime and evaluates to a Promise for that module's namespace object. A static import declaration is resolved before any code in the file runs and binds the exported names directly.

open as a page

In ES modules, what is the difference between a named export and a default export, and how does the import syntax differ for each?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A named export binds a value to a specific export name that importers must match exactly, using braces: import { parse } from './m.js'. A default export registers the value under the name "default", and the importer may bind it to any identifier it likes.

open as a page

In an ES module you write `var config = {}` and `function init() {}` at the top level. Are `globalThis.config` and `globalThis.init` defined afterwards, and how does that differ from the same two lines in a classic (non-module) script?

level: juniorimportance: must knowfreq 70%

basics

~10 s

No. Top-level declarations in an ES module live in that module's own scope, so globalThis.config and globalThis.init stay undefined. In a classic script the same declarations become properties of the global object.

open as a page

In an ES module `main.js` you write `console.log('main start');` and then, on the line below it, `import './dep.js';` — and `dep.js` logs `'dep evaluated'` at its top level. Which line prints first, and what rule decides that?

level: juniorimportance: must knowfreq 68%

basics

~20 s

The dependency prints first. Import declarations are hoisted and the whole module graph is linked before anything is evaluated, so dep.js runs to completion before any statement in main.js — including a statement written above the import.

open as a page

In a CommonJS module, what is the difference between `exports.parse = fn` and `module.exports = { parse: fn }`, and why does writing `exports = { parse: fn }` export nothing at all?

level: middleimportance: must knowfreq 74%

basics

~20 s

exports is a local variable that initially points at the same object as module.exports, and require() returns module.exports. Attaching properties through exports works; assigning a new object to exports only rebinds the local variable, so importers still get the original empty object.

open as a page

A page module statically imports a heavy charting library at the top. If you move it to `await import('./chart.js')` inside a button's click handler instead, what changes about what the browser downloads — and what happens if the user clicks that button five times?

level: middleimportance: must knowfreq 62%

basics

~20 s

The static import makes the library part of the initial module graph, so it is fetched and evaluated before the page code runs. Moving it into import() defers the fetch until the first click. Five clicks fetch and evaluate it once — later calls resolve from the module map.

open as a page

A module `counter.mjs` contains `export let count = 0;` and `export function increment() { count++; }`. Another ES module runs `import { count, increment } from './counter.mjs'; increment(); console.log(count);` — what is logged, and why?

level: middleimportance: must knowfreq 68%

basics

~20 s

It logs 1. An ES module import is a live, read-only binding to the exporter's own variable rather than a copy of its value, so reading count after increment() has run sees the exporter's updated value.

open as a page

An ES module contains no `'use strict'` pragma anywhere. Is its code strict, can you opt out, and does a function defined in that module stay strict when it is later called as a callback from non-strict code?

level: middleimportance: must knowfreq 60%

basics

~20 s

Module code is strict by definition, so the pragma is redundant and there is no way to opt out. Strictness is lexical, not caller-dependent: a function written inside a module stays strict wherever non-strict code later calls it.

open as a page

Why is `if (isDev) { import './debug.js'; }` a syntax error in an ES module, and why must the module specifier be a plain string literal rather than a variable or a concatenation?

level: middleimportance: must knowfreq 60%

basics

~20 s

ES module imports are static by design: an import declaration may appear only at the top level of a module, and its specifier must be a string literal. That lets the engine read the whole dependency graph out of the syntax and link it before running any code — which conditional or computed imports would make impossible.

open as a page

A CommonJS file `logger.cjs` ends with `module.exports = function log(msg) { console.log(msg); }`. In Node's native ESM, what does `import log from './logger.cjs'` bind, and why can the same import statement compiled to CommonJS by TypeScript or Babel end up binding `undefined` instead?

level: middleimportance: must knowfreq 68%

basics

~20 s

Node's ESM loader exposes a CommonJS module's entire module.exports value as its default export, so the import binds the function. A compiler instead rewrites the import to read a default property off the required object, which does not exist unless an interop helper wraps it.

open as a page

In an ES module, what does writing `await` directly in the module body (outside any function) do to that module and to the modules that import it?

level: middleimportance: must knowfreq 62%

basics

~10 s

Top-level await marks the module as asynchronous: its body runs only up to the await, resumes when the awaited promise settles, and every module importing it has its own evaluation held until that finishes.

open as a page

What counts as a top-level side effect in an ES module, and why does the presence of one force a bundler to keep code that nothing imports?

level: middleimportance: must knowfreq 55%

basics

~20 s

A top-level side effect is anything a module body does on evaluation that is observable outside itself: assigning to a global, mutating a built-in prototype, registering with a shared registry, or calling an opaque function. Analysis is conservative, so unprovable purity keeps the module.

open as a page

In an ES module you write `import { config } from './settings.js'` and later `config = {};`. What happens, and how is that different from writing `config.debug = true;`?

level: juniorimportance: should knowfreq 46%

basics

~20 s

Assigning to config fails: an imported name is a read-only binding, so the write is rejected with an error and never reaches the exporter. Setting config.debug = true works, because both modules point at the same object.

open as a page

In a CommonJS file, `const pkg = require('some-package')` returns an object shaped like `{ __esModule: true, default: [Function] }`, and calling `pkg()` throws `TypeError: pkg is not a function`. What produced that shape, and what does it tell you about how the package was built?

level: juniorimportance: should knowfreq 55%

basics

~20 s

The package was written as ES modules and compiled down to CommonJS. Compilers put an ESM default export on the exports object under a default property and add an __esModule marker, so the callable value is pkg.default, not pkg.

open as a page

Why is `await` outside any function a SyntaxError in a CommonJS file or a classic `<script>`, but legal in an ES module?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Top-level await exists only in the ES module goal, added in ES2022. A classic script or a CommonJS file is parsed as a script, where await is not a keyword outside an async function, and CommonJS loading is synchronous so a module could not suspend anyway.

open as a page

In a bundled ES-module app, an unused `import { formatDate } from './utils.js'` disappears from the production bundle, but `import './analytics.js'` — an import statement with no bindings at all — is kept. Why does the bundler treat these two import forms differently?

level: juniorimportance: should knowfreq 50%

basics

~20 s

An unused named binding can be deleted because static analysis proves no code reads it. A bindings-free import asks for the module's evaluation rather than for a name, so its top-level code is the requested effect and must still run.

open as a page

In Node's CommonJS, a.js sets `exports.done = false`, then calls `require('./b.js')`, and b.js immediately calls `require('./a.js')` and reads `.done` on the result. What does b.js receive, and why is no error thrown?

level: middleimportance: should knowfreq 48%

basics

~20 s

b.js receives a.js's partially populated exports object: done is false, and anything a.js assigns after its require call is simply absent. CommonJS caches the module object before running the body, so the back-edge require returns whatever exists at that instant — silently.

open as a page

In a native ES module graph, main.js imports './a.js'; a.js starts with `import { fromB } from './b.js'` and then declares `export const fromA = 'A'`; b.js starts with `import { fromA } from './a.js'` and logs fromA at its top level. What happens when this runs, and why?

level: middleimportance: should knowfreq 45%

basics

~20 s

b.js evaluates first and throws ReferenceError: Cannot access 'fromA' before initialization. Linking already created the binding for a.js's const, but a.js's body has not run yet, so the binding is still in its temporal dead zone.

open as a page

A CommonJS file counter.js contains `let count = 0; function increment() { count++; } module.exports = { count, increment };`. A consumer does `const { count, increment } = require('./counter')`, calls increment() twice, then logs count — and gets 0. Why, and how would you expose a value that actually tracks?

level: middleimportance: should knowfreq 52%

basics

~20 s

The exported object stored a copy of the number at the moment the module ran, so its count property has no link to the module's variable, and destructuring copies that stale value again. Expose an accessor or a getCount() function instead.

open as a page

A JavaScript library publishes both a CommonJS build and an ES module build of the same source. What is the "dual package hazard", and how can one Node.js process end up running that library's code twice?

level: middleimportance: should knowfreq 38%

basics

~20 s

One library ends up loaded twice in a process — once from its CommonJS build, once from its ESM build — because require and import resolve the same package name to two different files. Each copy gets its own module state and its own class identities.

open as a page

What is `import.meta` in an ES module, what does `import.meta.url` typically give you, and why does the same line throw a SyntaxError when the file is loaded as a classic script?

level: middleimportance: should knowfreq 38%

basics

~20 s

import.meta is a per-module object the host fills with metadata about the currently running module. In browsers and Node ESM its url property holds the module's own URL, useful for resolving sibling assets. It is module-only syntax, so a classic script rejects it at parse time.

open as a page

In an ES module, what does `export * from './utils.js'` actually re-export, and what does it leave out?

level: middleimportance: should knowfreq 50%

basics

~10 s

It forwards every named export of ./utils.js under the same names, and deliberately excludes that module's default export. It also creates no local bindings, so the re-exporting file cannot use those names itself.

open as a page

In an ES module barrel file, what is the difference between writing `export { parse } from './parser.js'` and writing `import { parse } from './parser.js'; export { parse };`?

level: middleimportance: should knowfreq 38%

basics

~20 s

Consumers see the same export either way, but the from-form creates no local binding: the barrel itself cannot reference parse. The import-then-export version does bind parse in module scope, so the file can also use the value.

open as a page

At the top level of an ES module, what is the value of `this`, and what breaks in older library code written on the assumption that top-level `this` is the global object?

level: middleimportance: should knowfreq 52%

basics

~20 s

Top-level this in an ES module is undefined, not the global object. Library code that assigns its export onto this, or sniffs the environment through this, throws a TypeError or silently detects nothing once the file is loaded as a module.

open as a page

Transpiled CommonJS output often begins with `Object.defineProperty(exports, "__esModule", { value: true })`. What is that marker for, and how do interop helpers such as Babel's `_interopRequireDefault` or the `__importDefault` helper emitted by TypeScript use it?

level: middleimportance: should knowfreq 42%

basics

~20 s

It is a tooling convention marking a CommonJS exports object as lowered ES module output, so its default property is a real default export. Interop helpers read it to decide whether to pass the object through or wrap it as { default: value }.

open as a page

Given `a.js` containing `console.log('a1'); await null; console.log('a2');`, `b.js` containing `console.log('b');`, and `main.js` containing `import './a.js'; import './b.js'; console.log('main');`, what does running `main.js` as an ES module print, and why?

level: middleimportance: should knowfreq 40%

basics

~10 s

It prints a1, b, a2, main. The await in a.js suspends only that module, so the independent sibling b.js evaluates immediately; main.js runs last because it waits for both dependencies to finish.

open as a page

An app imports a single function from a barrel `index.js` whose body is nothing but `export * from './...'` lines covering forty modules, yet most of those modules still end up in the production bundle. What about that barrel undermines tree shaking, and how would you change it?

level: middleimportance: should knowfreq 45%

basics

~20 s

A barrel makes every re-exported module part of the graph, so each must be parsed and, unless its top level is provably inert, evaluated. One impure module among forty is enough to pull itself in, and re-export chains amplify the cost.

open as a page

showing 1–30 of 49