skip to content

CommonJS Module Semantics

The minimum CommonJS model you need before interop makes sense: require blocks until the module finishes, and what you get back is whatever object module.exports pointed at when it did. Interviewers love the `exports = ...` versus `module.exports = ...` trap.

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

questions

4

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 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 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 CommonJS service takes several seconds to start serving traffic, and the time is spent in top-level code of required modules — parsing config, building lookup tables — before the server ever starts listening. Why does require() put that cost exactly there, and how would you restructure the modules?

level: seniorimportance: should knowfreq 36%

basics

~20 s

require() runs a module's body to completion synchronously before returning, so anything at a module's top level executes during the require call and blocks the single thread. Move expensive work into exported init or factory functions the application calls deliberately.

open as a page