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?
answer
- two builds, one package name
- require path versus import path
- caches key on file, not name
- state and class identity duplicated
- singletons initialised twice
basics
~20 sOne 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.
solid answer
~50 sA dual-published package ships two physical builds of the same source, say `dist/index.cjs` and `dist/index.mjs`. The runtime picks which file to load based on how the package was reached: code that `require`s it lands on the CommonJS file, code that `import`s it lands on the ESM file. If both paths happen in one process — your app uses `import`, but a transitive dependency still uses `require` — the process holds two completely independent module records. They are separate files, so neither module cache deduplicates them: every top-level `let`, every `Map` used as a registry, every class, and every symbol created with `Symbol()` exists twice. Concretely, a singleton is initialised twice, configuration set through one copy is invisible to the other, and `instanceof` against a class from copy A fails for an object built by copy B.
go deeper
Be able to say that require and import can pull two different files from the same package, and that each file gets its own variables. Recognising that a library can exist twice in one process is enough at this level.
Explain the mechanism: caches key on the resolved file, the package ships two build files, so both are evaluated. Name the concrete casualties — module-level state, class identity, unique symbols — and say why pure functions are unaffected.
Show you would spot this in production: a singleton that initialises twice, config that does not stick, an intermittent instanceof failure far from the library. Be ready to say how you would confirm which files were actually resolved.
Frame it as a contract question for library authors: any public API whose correctness depends on identity or shared state imposes a single-instance requirement on consumers. Own the decision of whether your package may ship two builds at all.
## What "dual package" means Many libraries publish more than one build of the same source: a CommonJS build for consumers that call `require()`, and an ES module build for consumers that use `import`. These are two *different files* on disk — for example `dist/index.cjs` and `dist/index.mjs` — produced from the same source but shipped side by side. The package tells the runtime which file to hand out depending on how the package was reached. The *dual package hazard* is the failure mode that follows: if both entry paths are exercised in a single process, the process ends up holding two independent instantiations of the same library. ## Why the module caches do not save you A common first reaction is "modules are cached, so it can only run once." Caching is real, but it is keyed by the **resolved file**, not by the package name you typed. CommonJS caches by resolved filename; the ESM registry caches by resolved URL. When one specifier resolves to `dist/index.cjs` for one importer and `dist/index.mjs` for another, those are two distinct keys, so both are loaded, evaluated, and kept. Nothing in the language deduplicates "the same library expressed as two files." ## What "two copies" costs you Every piece of module-level state is duplicated: ```js // the library's source — one counter per module instance let count = 0; export function next() { return ++count; } ``` ```js // the app, mixing entry paths import { next } from 'pkg'; // -> dist/index.mjs const { next: next2 } = require('pkg'); // -> dist/index.cjs next(); // 1 next2(); // 1, not 2 — two counters ``` The same duplication hits: - **Singletons and caches** — a connection pool, a plugin registry, a memoisation `Map`. Two instances, each half-populated. - **Configuration** — `lib.configure({...})` called on one copy leaves the other on defaults. - **Class identity** — `class ValidationError extends Error {}` evaluated twice produces two unrelated constructors that merely share a name, so `instanceof` across copies is `false`. - **Unique symbols** — `Symbol('token')` is fresh per evaluation, so symbol-keyed protocols between copies stop matching. (`Symbol.for('token')` uses the realm-wide registry and *does* survive, which is why it is a common mitigation.) What is *not* duplicated: pure functions with no captured state, and plain data with no identity requirement. A library of stateless helpers can be loaded twice with no observable consequence beyond a little memory and start-up time — which is exactly why the hazard is invisible until a stateful library hits it. ## Why mixed entry paths are so easy to get You rarely mix `import` and `require` for the same package deliberately. It happens because your dependency graph is mixed: your own code is ESM, but some transitive dependency is still CommonJS and pulls the CommonJS build of a shared library. The two consumers never see each other, so the bug surfaces far from its cause. The same shape appears at bundle time. If a bundler resolves a dependency through the ESM entry for one part of the graph and through the CommonJS entry for another, the output bundle simply contains both copies — same hazard, no runtime involved. ## Distinguishing it from a plain duplicate install Two *versions* of a package installed at different points in `node_modules` also give you two copies with all the same symptoms. That is a different cause (version resolution) with a different fix (dedupe or align the version range). The dual package hazard is the case where **one installed copy at one version** still yields two module instances, because the package itself ships two builds. Checking whether the installed tree really contains one version is therefore the first step in telling the two apart. ## Practical rule of thumb The hazard is a *module identity* problem, not a syntax problem. Ask what the library's public contract depends on: if it depends only on the shape of values it returns, duplication is harmless. If it depends on identity — `instanceof`, a shared registry, a single connection, a symbol used as a private key — then two copies is a correctness bug, and the library must be built so only one implementation can ever be instantiated.
- If a library is a set of pure functions with no module-level state, does the dual package hazard still matter?Barely. Loading it twice costs a little memory and start-up work, but nothing observable changes because there is no state to diverge and no identity to compare. The hazard only becomes a correctness bug when the library exposes something identity-bearing — a class checked with `instanceof`, a shared registry, a pool, or a unique symbol used as a key.
- How would you tell a dual package hazard apart from simply having two versions of the package installed?Check the installed tree first: if the package resolves to a single version in a single location, version duplication is ruled out and two builds of that one copy is the remaining explanation. Comparing the file paths the two consumers actually resolved settles it — same directory, different build files means the dual package hazard; different directories means duplicate installs.
- Can this happen in a browser bundle where there is no require() at runtime?Yes. The mixing happens at build time: if the bundler resolves the package through its ESM entry for part of the graph and through its CommonJS entry for another part, both files are included and both are evaluated in the browser. The runtime never sees `require`, but the bundle still contains two independent copies with two sets of state.
It is like a company printing two identical staff handbooks and letting each department pencil in its own amendments: the wording matches, but the two books drift apart, and nobody notices until two departments quote different rules.
saying these in an interview costs you the question
- The module cache guarantees a library runs only once
- It is the same file, so state must be shared
- This only happens when two versions are installed
- Two classes with the same name are the same class
- It affects only TypeScript projects