skip to content

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%

answer

  1. two references, one object
  2. only one of them is read back
  3. mutating versus rebinding
  4. require never looks at the variable
  5. assignment rebinds, it does not reach through

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.

solid answer

~50 s

Every CommonJS module starts with `module.exports` set to a fresh empty object and a local variable `exports` initialised to that same object — two references, one object. `exports.parse = fn` mutates the shared object, so it is visible through `module.exports` and therefore to importers. `module.exports = { parse: fn }` replaces the object in the slot that `require` actually reads, which is also fine. But `exports = { parse: fn }` is a plain variable assignment: it rebinds a module-local variable and leaves `module.exports` still pointing at the untouched empty object, so `require` returns `{}` and the caller sees nothing. The rule that resolves all of it is that `require` returns `module.exports` and never looks at the `exports` variable. Mixing the two styles is also a trap: an assignment to `module.exports` discards any properties previously attached via `exports`.

code

javascript · 8 lines
javascript
// Reassigning the alias detaches it from the slot require reads.
exports = { parse: () => 'never seen' };
console.log(exports === module.exports); // false
console.log(module.exports);             // {}

// Both working forms, for contrast.
module.exports.parse = (s) => s.trim();  // attach to the object in the slot
module.exports = { parse: (s) => s.trim() }; // replace the object in the slot

go deeper

for a junior

Remember the safe habit: attach with exports.name = value, or assign once with module.exports = value, and never write exports = something. Be able to say that require gives back module.exports.

for a middle

Explain the mechanics out loud: one object, two references, and a slot that require reads at the end of the body. Show why assignment to the alias is an ordinary variable rebind that leaves the slot untouched.

for a senior

Demonstrate the diagnosis path: an empty exports object surfaces one file away as a TypeError in the consumer, so describe how you would trace it back and what convention or lint rule you would adopt to make the whole class of bug impossible.

for a principal

Own the convention decision. Argue for a single house style per codebase, enforced by lint rather than review, and explain why silent-failure modes like this deserve tooling even though each individual instance is a five-minute fix.

## Two names, one object — at first Before a CommonJS module's body runs, the host prepares a `module` object for the file and sets `module.exports` to a fresh empty object. It also introduces a module-local variable named `exports`, initialised to that same object. At the first line of your file, `exports === module.exports` is `true`. They are two references to one object, not two names for one storage slot. The distinction between *the object* and *the slot* is the whole question. `require` reads the slot: when the body finishes, it hands back the value stored in `module.exports`. It has no interest in your `exports` variable, which is invisible outside the module. ## The three forms **Attaching a property.** `exports.parse = fn` reaches through the reference and mutates the shared object. Because `module.exports` still points at that object, the property is visible to importers. `module.exports.parse = fn` is exactly equivalent while the two still agree. **Replacing the exports object.** `module.exports = { parse: fn }` writes a new object into the slot `require` reads, so importers get the new object. This is the form you need whenever the module's public value is not a plain namespace bag — a single function, a class, a constant. **Reassigning the alias.** `exports = { parse: fn }` is a bare assignment to a local variable. It changes what `exports` points at inside the file and nothing else. `module.exports` still holds the original empty object, so importers get `{}`: ```js // broken.js exports = { parse: () => 'nope' }; console.log(exports === module.exports); // false console.log(module.exports); // {} — this is what require returns ``` The failure is silent at the definition site: nothing throws, nothing warns, and the module looks correct on a skim. The symptom appears in the consumer as `TypeError: parse is not a function`, one file away from the cause. ## Why the mistake is so easy to make The two working forms make `exports` look like a first-class export mechanism, so it is natural to assume assigning to it is just the bulk version of attaching to it. It is not, and the reason is ordinary JavaScript reference semantics rather than anything module-specific: assigning to a variable that holds an object reference rebinds the variable; it never reaches through to whatever else references the object. Anybody who has been bitten by ```js function reset(obj) { obj = {}; } // caller's object is untouched ``` has already met the same rule in a different costume. ## Mixing the two styles Order matters when a file uses both forms: ```js exports.a = 1; // attaches to the original object module.exports = { b: 2 }; // replaces the slot; `a` is now unreachable // importers receive { b: 2 } ``` And after replacing `module.exports`, further use of `exports` is dead code, because `exports` still references the object that is no longer in the slot: ```js module.exports = class Repo {}; exports.helper = () => {}; // attaches to the abandoned object — invisible ``` This is a real review finding, not a puzzle: it typically shows up when a file grows a class export at the top and someone later adds a helper with the `exports.` habit further down. ## Practical guidance Pick one style per file. If the module exports a bag of named things, use `exports.x = ...` consistently and never assign to `exports`. If the module's value *is* one thing, assign `module.exports = ...` once, and from then on attach anything extra to `module.exports`, not to `exports`. Some teams sidestep the whole trap by never using the `exports` variable and always writing `module.exports.x = ...`, which costs a few characters and removes an entire class of silent bug. If you want a guard while debugging, `console.log(exports === module.exports)` at the end of a module tells you immediately whether the alias has been detached. ## How to answer it in an interview State the invariant first — `require` returns `module.exports`, full stop — then explain that `exports` is a convenience reference to the object that slot initially holds. Every case follows from those two sentences: mutation through either reference is visible, assignment to the slot is visible, assignment to the variable is not.

  • After a file does `module.exports = class Repo {}`, does a later `exports.helper = fn` in the same file do anything useful?
    No. The `exports` variable still references the original seeded object, which is no longer the value in `module.exports`, so the property is attached to an object nobody can reach. Importers get the class and nothing else. Once you replace `module.exports`, every further export must go through `module.exports` too.
  • A file writes `exports.a = 1` and then `module.exports = { b: 2 }`. What does an importer receive, and why?
    Just `{ b: 2 }`. The first line attached `a` to the object originally in the slot; the second line put a different object in the slot, and `require` reads the slot at the end of the body. The earlier property is not merged in — nothing merges the old and new exports objects.
  • How would you catch this class of mistake before it reaches a consumer?
    Lint for it: rules that ban assigning to `exports` exist in the common Node lint plugins, and a convention of only ever writing `module.exports` removes the alias entirely. Failing that, a unit test that requires the module and asserts on its shape catches an empty exports object immediately, which is much closer to the cause than a `TypeError` in a consumer.

saying these in an interview costs you the question

  • Says exports and module.exports are interchangeable in all cases
  • Thinks assigning to exports replaces the module's exported value
  • Believes require() returns the exports variable
  • Assumes properties attached before a module.exports assignment are merged in
  • Claims reassigning exports throws or warns at load time

context