Dual Package Hazard in ESM and CommonJS Explained

The dual package hazard is a critical architectural issue in the Node.js ecosystem that occurs when a package provides both ECMAScript Modules (ESM) and CommonJS (CJS) versions of its codebase. When an application or its dependencies simultaneously load the ESM version via import and the CJS version via require(), Node.js treats them as two completely separate modules. This article explains how the dual package hazard arises, the runtime bugs it creates, and how package authors can avoid it.

How the Hazard Arises

Node.js manages ESM and CommonJS modules through two independent resolution and caching systems. When a module is loaded using import, it is resolved and stored in the ESM module graph. When a module is loaded using require(), it is resolved and stored in the CommonJS require.cache.

The hazard typically emerges when a package author configures conditional exports in package.json to support both formats:

{
  "name": "my-library",
  "exports": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs"
  }
}

If an application imports my-library directly using ESM syntax, Node loads ./dist/index.mjs. If a third-party dependency used by that application requires my-library using CommonJS syntax, Node loads ./dist/index.cjs. Because Node.js does not recognize that these two files represent the same logical package, both files are executed and kept in memory simultaneously.

Consequences of the Dual Package Hazard

When two separate instances of a package run concurrently within the same process, several breaking behaviors can occur:

1. Broken Singletons and State Duplication

If a library maintains internal state—such as caches, connection pools, or registry tables—loading both versions creates two distinct instances of that state. Updates made by the ESM code will not be visible to the CJS code, leading to desynchronization and difficult-to-trace bugs.

2. instanceof Failure

When a library exports a class, the ESM and CommonJS builds create two separate constructor functions with different prototype references. An object instantiated from the CJS version will fail an instanceof check when evaluated by code using the ESM version:

import { CustomError } from 'my-library'; // ESM instance
const { produceError } = require('my-dependency'); // Uses CJS my-library

const error = produceError();
console.log(error instanceof CustomError); // Evaluates to false

3. Increased Memory Footprint

Duplicate module execution inflates the application’s memory usage and increases initialization time, as initialization scripts, schemas, and static data are parsed and stored twice.

How to Prevent the Hazard

Package authors can prevent the dual package hazard using several strategies: