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;`?
answer
- binding versus object
- who owns the variable
- modules are always strict code
- one writer, many readers
- frozen only if you froze it
basics
~20 sAssigning 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.
solid answer
~40 s`config = {}` does not work. An imported name is an immutable binding into the exporting module's scope, so the assignment is rejected — depending on the engine it is reported when the module is compiled or when the line runs, but it never succeeds and the exporter's variable is untouched. Because module code is always strict, the failed write raises an error rather than silently doing nothing. `config.debug = true` is a completely different operation: read-only applies to the *binding*, not to the object it refers to. That line mutates the shared object, and every module that imported it observes the new property immediately. The practical rule is that only the exporting module may rebind an export, while anyone holding an exported object can mutate its contents.
go deeper
Be able to say plainly that you cannot assign to an imported name, but you can set properties on an imported object. Naming the binding-versus-object distinction is most of the answer at this level.
Explain that the import is an immutable indirect binding into the exporter's scope, that only the exporting module may rebind it, and that module code being strict is why the failed write errors instead of doing nothing.
Show judgment about API shape: if consumers must change state, expose a function that writes inside the owning module, and decide deliberately whether an exported object should be frozen or kept private behind accessors.
Argue the design point — immutable imports keep every write to a value inside one file, which is what makes an export graph auditable. Weigh that against the convenience of a shared mutable config object and set a convention accordingly.
## Two different things are being written The two lines look similar and are not related at all. - `config = {}` targets the **binding** — the name-to-slot association the import created. That is the thing ES modules make read-only. - `config.debug = true` targets the **object** the slot currently points at. Nothing about modules protects that object. Confusing the two is the single most common misunderstanding about import mutability. ## Why the reassignment is rejected When a module graph is linked, each named import becomes an indirect binding into the exporting module's environment: the importer's `config` and the exporter's `config` name the same storage slot. The spec makes those import bindings immutable, so the importing module has read access only. ```js // settings.js export let config = { debug: false }; export function replaceConfig(next) { config = next; } // the exporter may rebind ``` ```js // main.js import { config, replaceConfig } from './settings.js'; config = {}; // rejected — cannot assign to an imported binding replaceConfig({}); // fine — the write happens inside the owning module ``` Engines differ in *when* they complain — some detect the assignment while compiling the module, some raise it as the statement executes — but the outcome is identical everywhere: the write never lands, and it is never silently swallowed. Module code is always strict, and a rejected write in strict mode is an error rather than a no-op. This is also why exported values feel `const`-like from the outside even when the exporter declared them with `let`. The exporter chose `let` for itself; the importer gets read access regardless. ## Why the property write succeeds `config.debug = true` never touches the binding. It reads `config` (allowed), gets a reference to an object, and writes a property on that object. The object is shared: both modules hold references to the same one, so the mutation is visible to every importer and to the exporter, immediately and without any module machinery being involved. ```js import { config } from './settings.js'; config.debug = true; // works — plain object mutation console.log(config.debug); // true, everywhere ``` If you want to prevent that too, the exporter has to make the object itself resistant — for example by exporting a frozen object, or by exporting only a getter function and keeping the object private. Module syntax alone gives you no protection here. ## Why the language draws the line where it does Making imports immutable keeps the export graph analysable: a name's writer is always the module that declared it, so tooling and readers can find every mutation by looking in one file. If any importer could rebind an export, a value's history would be scattered across the whole program, and the guarantee that a namespace object's exports correspond to real declarations would break down. Note that the restriction is about *who writes*, not about *when*: the exporter may reassign whenever it likes, and importers see the new value on their next read, because the binding is live. ## What to do when consumers need to change the value Export a function. `replaceConfig(next)` above performs the write inside the module that owns the variable, which is legal, greppable, and works identically under any module system. A second common shape is to export a single container object and let consumers write fields on it — legal because it is property mutation, though it gives up the "one writer" property that made imports immutable in the first place. ## The trap to remember Because `config.debug = true` is legal, teams sometimes conclude imports are mutable and design around it, then get an error the first time someone reassigns an import in a refactor. Say the rule crisply in an interview: **the binding is read-only, the object is not.**
- How can the exporting module make even the property write fail?By hardening the object rather than the binding — for example exporting `Object.freeze({ debug: false })`, so property writes are rejected in strict module code. A stronger option is to keep the object module-private and export accessor functions, so consumers never hold the reference at all. Module syntax alone protects only the binding.
- If imports are read-only, how can an exported value ever change?Only the exporting module can rebind it, and it can do so at any time — for example inside an exported function or during its own startup. Importers see the new value on their next read, because the binding is live. Read-only restricts who writes, not whether the value can change.
saying these in an interview costs you the question
- Thinks imports are fully immutable, including the object's contents
- Believes reassigning an import updates it just locally
- Assumes the failed assignment is silently ignored
- Says export let makes the import writable by consumers
- Claims freezing the binding is possible with syntax alone