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?
answer
- when was the value read?
- primitives copy, objects share
- the object literal read it once
- destructuring reads it again
- export a getter or a function
basics
~20 sThe 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.
solid answer
~50 sTwo independent copies happen here. When `module.exports = { count, increment }` runs, the object literal reads the current value of `count` — zero — and stores that number as a property; the property is not a link back to the variable. Then `const { count } = require(...)` reads that property once and binds a third copy into the consumer. `increment()` still updates the module's own `count` variable, which nothing outside can observe, so the consumer's `count` stays 0 forever. The fix is to export something that is evaluated on access rather than at definition: an accessor property like `get count() { return count; }`, or a `getCount()` function. Note the object identity itself is shared — mutating `module.exports.count` from inside the module would be visible to anyone holding the object — but that only helps if the module updates the property rather than a closure variable.
code
javascript · 16 lines// Stale: the property captured the number when the module ran.
let a = 0;
const stale = { a, bump: () => { a++; } };
stale.bump();
console.log(stale.a); // 0
// Live: the getter runs on every read.
let b = 0;
const live = { get b() { return b; }, bump: () => { b++; } };
live.bump();
console.log(live.b); // 1
// ...but destructuring calls the getter once and stores the result.
const { b: snapshot } = live;
live.bump();
console.log(snapshot, live.b); // 1 2go deeper
Recall that copying a number out of an object gives you an independent value that will not update. If you need a changing value from a module, call a function that returns it rather than reading a number once.
Walk through both copy points precisely: the object literal captures the variable's current value, and destructuring captures the property's current value. Then propose an accessor or reader function and explain why each works.
Show the diagnosis discipline — distinguish a stale property on a shared object from a module that swapped its exports object entirely, since both present as 'my import is stale'. Recommend module shapes that make the failure impossible rather than documented.
Treat mutable cross-module state as an interface decision. Argue for exposing behaviour rather than values, decide whether exported surfaces should be frozen, and weigh the debuggability cost of modules whose published data changes after load.
## Two copies, not one It is tempting to describe this as a single "exports are copied" rule, but there are two distinct copy steps and a candidate needs to separate them. **Copy one, inside the module.** `module.exports = { count, increment }` is an object literal using shorthand property syntax, which is just `{ count: count, increment: increment }`. Evaluating it reads the current value of the `count` variable — the number `0` — and stores that number as a property of a new object. Numbers are primitives; the property holds a value, not a reference to the variable's storage. From that instant, `module.exports.count` and the module-scoped `count` variable are unrelated. `increment()` closes over the variable, so it keeps updating the variable — and nothing propagates that to the property. **Copy two, in the consumer.** `const { count, increment } = require('./counter')` reads the property once, at destructuring time, and binds the result to a new `const`. Even if the module *had* kept the property up to date, this local `const` would still be a snapshot taken at require time. So the consumer's `count` is a copy of a copy, and both copies were taken before any `increment()` call happened. ## What is actually shared What the module and the consumer share is the *object identity*. `require` hands back the very object the module built — not a clone. That means property mutation is mutually visible: ```js // counter.js const state = { count: 0 }; function increment() { state.count++; } module.exports = { state, increment }; // consumer.js const { state, increment } = require('./counter'); increment(); console.log(state.count); // 1 — same object, property read on access ``` Here the consumer destructured an object reference rather than a number, so it still points at the live state, and reading `state.count` happens after the mutation. The lesson generalises: reads that happen at access time see current data, reads that happen at binding time see a snapshot. ## Making the value observable The cleanest fixes turn the read into something that runs later. **An accessor property** keeps the ergonomic `counter.count` syntax: ```js let count = 0; module.exports = { increment() { count++; }, get count() { return count; }, }; ``` A consumer holding the exports object sees a current value every time it reads `counter.count`, because the getter runs on each access. The trap survives one level, though: `const { count } = require('./counter')` *invokes* the getter once and stores the resulting number, so destructuring still produces a snapshot. If you export an accessor, document that consumers must keep the object. **An explicit function** makes that impossible to get wrong: ```js module.exports = { increment, getCount: () => count }; ``` Destructuring `getCount` copies a function reference, and the closure reads the variable at call time. This is the most robust shape, and it is why so many CommonJS modules expose `getX()` rather than `x`. **A mutable state object** — the `state` example above — works too, and is common where several values move together, at the cost of exposing a writable surface. ## Where this actually bites The realistic version of this bug is not a counter. It is a config module that assigns `module.exports = { apiUrl: process.env.API_URL, ... }` at load time, followed by a consumer that destructures it, and then something that tries to change configuration later and finds the change invisible. Or a module exporting an `isReady` boolean that flips after asynchronous initialisation, where every consumer captured `false`. The diagnosis is the same in each case: ask *when was this value read*, and count the copies between the source of truth and the reader. ## Debugging it When a value is stubbornly stale, log the identity rather than the value. If `require('./counter') === require('./counter')` style checks are not available to you, log the exports object itself at the point of use and compare it with what the module holds. If the object matches but the property is stale, the property is a copy of a variable. If the object itself differs from what you expect, something reassigned `module.exports` after consumers had already captured it — a different failure with the same symptom. ## Interview framing Strong answers name the two copy points explicitly and then propose the accessor or function fix, ideally noting that destructuring an accessor re-introduces the snapshot. Weak answers say vaguely that "CommonJS copies exports", which is not quite true — the object is shared; it is the primitive values read out of it that are copies.
- If the module exported a getter instead, would destructuring in the consumer fix the problem?No. Destructuring reads the property, which invokes the getter once and stores the returned number in a local binding. The consumer must keep the exports object and read `counter.count` each time for the getter to help. That fragility is the main argument for exporting a `getCount()` function instead — copying a function reference is harmless.
- If a consumer assigns to a property on the object it got from require(), does the module see it?Yes — `require` hands back the same object the module holds, so property writes are mutually visible. That is occasionally useful and more often a hazard: any consumer can mutate another module's public surface. `Object.freeze(module.exports)` at the end of the body is the usual defence when the exported shape is meant to be constant.
- How would you diagnose a value that a consumer insists is stale?Ask when the value was read. Log the exports object at the point of use and compare identity with what the module holds. Matching object but stale property means the property is a copy of a variable; a different object means something reassigned `module.exports` after that consumer had already captured it. The two failures look identical from the consumer's side.
saying these in an interview costs you the question
- Says CommonJS deep-copies the exports object for each consumer
- Thinks a property stays linked to the variable it was initialised from
- Believes calling the exported function refreshes the exported number
- Assumes destructuring an accessor keeps it live
- Blames the module system rather than value-versus-reference semantics