Jest’s native ESM support has been a game-changer, but getting it right, especially when dealing with mixed CJS/ESM environments or older Node.js versions, can still trip you up. The core issue is how Jest, historically a CommonJS-centric tool, resolves and executes ES Module import and export statements.

Common Causes and Fixes for Native ESM Support in Jest

  1. jest-circus Not Found or Incorrect Jest Version:

    • Diagnosis: A common error message is ReferenceError: Cannot find module 'jest-circus'. This often stems from an outdated Jest installation or a conflict between Jest and its internal dependency jest-circus which handles the ESM execution.
    • Fix: Ensure you have a recent version of Jest installed. For Jest v28+, native ESM support is built-in. Run npm install --save-dev jest@latest or yarn add --dev jest@latest.
    • Why it works: Newer Jest versions bundle jest-circus correctly and have the necessary infrastructure to parse and run ESM test files.
  2. type: "module" in package.json Conflicts with CJS Dependencies:

    • Diagnosis: You’ll see errors like TypeError [ERR_UNKNOWN_MODULE_FORMAT]: Unknown file format: .js or unexpected require() failures when Jest tries to import a CommonJS module that was incorrectly interpreted as ESM. This happens when your package.json declares "type": "module", making all .js files ESM by default, but you still have CJS dependencies that don’t play nice.
    • Fix:
      • Option A (Recommended for new projects): Keep "type": "module" and explicitly rename CJS files to .cjs or use .mjs for ESM files if you need to interop with CJS.
      • Option B (For existing CJS projects): Remove "type": "module" from your package.json. Then, explicitly mark your test files as ESM by renaming them to .mjs.
      • Option C (Less ideal): If you must keep "type": "module", configure Jest’s transform to handle .js files.
    • Why it works: This ensures Node.js (and by extension, Jest) correctly identifies module types. By defaulting to ESM ("type": "module") and using .cjs for CJS, you create a clear separation. Conversely, removing "type": "module" makes .js files CJS by default, requiring explicit .mjs for ESM tests.
  3. transform Configuration for .js or .mjs Files:

    • Diagnosis: Errors like SyntaxError: Unexpected token 'export' or SyntaxError: Unexpected token '<' (when parsing HTML instead of JS) indicate Jest isn’t transforming your ESM syntax. This is common if you’re using a Babel transform for ESM.
    • Fix: Configure Jest to use its built-in ESM transformer or a Babel transform.
      • Native ESM (Jest 28+): Ensure testEnvironment: "node" (or jsdom) and no custom transform for .js or .mjs files if they are your ESM tests. Jest will handle them natively.
      • Babel Transform: If you need Babel for older Node versions or specific transpilation, configure jest.config.js:
        // jest.config.js
        module.exports = {
          transform: {
            '^.+\\.(js|mjs|ts|tsx)$': ['babel-jest', {
              // Your Babel presets/plugins here, e.g.:
              presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
              plugins: ['@babel/plugin-transform-runtime'],
            }],
          },
          // ... other config
        };
        
        Make sure you have @babel/preset-env and babel-jest installed.
    • Why it works: The transform option tells Jest which preprocessor to use for specific file extensions. For native ESM, Jest handles it automatically. For Babel, it ensures that ESM syntax is transpiled into a format Jest understands.
  4. testEnvironment Setting:

    • Diagnosis: Errors like ReferenceError: document is not defined (when running in a Node environment) or ReferenceError: require is not defined (when trying to require in a browser-like jsdom environment) can occur if your testEnvironment doesn’t match your module type or test needs.
    • Fix: Set testEnvironment: "node" in your jest.config.js if your tests primarily interact with Node.js APIs or if you’re relying on Node’s native ESM resolution. If your tests are DOM-heavy, use "jsdom". Jest v28+ supports ESM in both environments.
      // jest.config.js
      module.exports = {
        testEnvironment: "node", // or "jsdom"
        // ... other config
      };
      
    • Why it works: The testEnvironment dictates the global scope and available APIs for your tests. "node" provides Node.js globals and resolution, while "jsdom" provides a simulated browser DOM. Native ESM support in Jest v28+ is designed to work correctly within these environments.
  5. moduleNameMapper and ESM Resolution:

    • Diagnosis: If you’re using moduleNameMapper to alias modules and encounter Module not found errors for ESM imports, the mapping might be incorrect or not respecting ESM resolution rules.
    • Fix: Ensure your moduleNameMapper regex correctly targets the files and that the replacement path is valid. For ESM, Jest’s internal resolver should handle most cases, but complex mappings might need adjustment. Example:
      // jest.config.js
      module.exports = {
        moduleNameMapper: {
          '^@/(.*)$': '<rootDir>/src/$1',
        },
        // ... other config
      };
      
    • Why it works: moduleNameMapper intercepts module resolution. For ESM, Jest’s resolver still applies after the mapping, so the target of the alias must be a resolvable module path.
  6. --experimental-vm-modules Flag (for older Node versions or specific setups):

    • Diagnosis: If you’re running Jest via a custom script that invokes Node.js directly and encountering ESM errors, you might be missing the Node.js flag that enables ESM support.
    • Fix: When running Jest from the command line or in a script, prepend the Node.js flag:
      node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js
      # Or if using a package.json script:
      # "scripts": {
      #   "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
      # }
      
      Note: This flag is generally not needed for Jest v28+ when running jest directly, as Jest manages its own Node.js execution context. It’s more for manual Node.js invocations or older Jest versions.
    • Why it works: The --experimental-vm-modules flag is a Node.js runtime option that enables the experimental ES Module support within Node.js itself, which Jest leverages.

After fixing these, the next hurdle is often dealing with dynamic import() calls in tests, which can require specific Jest configurations or Jest mocks to handle properly.


Understanding Jest’s Native ESM Support

The most surprising thing about Jest’s native ESM support is that it doesn’t fundamentally change how you write your tests, but rather how Jest executes them. You can write import and export statements directly in your test files, and Jest (v28+) will handle them without needing a separate transpilation step like Babel for basic ESM syntax.

Let’s see it in action with a simple module and a test.

src/math.js (ES Module)

// src/math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

src/math.test.js (ES Module Test)

// src/math.test.js
import { add, subtract } from './math.js'; // Note the .js extension is often required for native ESM resolution

describe('Math Functions', () => {
  test('should add two numbers correctly', () => {
    // This is a standard Jest test, but it imports from an ESM module
    expect(add(5, 3)).toBe(8);
  });

  test('should subtract two numbers correctly', () => {
    expect(subtract(10, 4)).toBe(6);
  });
});

jest.config.js (Minimal configuration for native ESM)

// jest.config.js
module.exports = {
  // Jest v28+ supports ESM out of the box.
  // Ensure your test files are .js or .mjs and your package.json
  // has "type": "module" or you are correctly handling CJS interop.

  // For Node.js environment (most common for backend/utility tests)
  testEnvironment: 'node',

  // If you have both .js (CJS) and .mjs (ESM) files and want Jest
  // to treat .mjs as ESM and .js as CJS, this is usually automatic
  // with "type": "module" in package.json.
  // If "type": "module" is NOT in package.json, .js is CJS, and you'd
  // use .mjs for your ESM tests.

  // If you're explicitly using .mjs for your test files:
  // testMatch: ['**/__tests__/**/*.mjs', '**/*.test.mjs'],

  // If your source files are also ESM and you need Jest to resolve them:
  // If package.json has "type": "module", Jest will treat .js as ESM.
  // If not, you might need to specify module extensions or use .mjs.
  // moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node', 'mjs'],
};

Running the Test:

If your package.json has "type": "module", Jest will generally pick up src/math.js as an ES Module and src/math.test.js will import it correctly.

npm test
# or
yarn test

Internal Mechanism:

When Jest encounters an import statement in a file it’s processing for tests, it checks the file’s type. If it’s an ES Module (determined by Node.js’s module resolution, often influenced by package.json’s "type": "module" or file extensions like .mjs), Jest uses Node.js’s built-in VM module (vm.Module) or a similar internal mechanism to parse and execute the module. It doesn’t need to pass the file through Babel for basic import/export syntax. This is significantly faster and more accurate for modern JavaScript.

The testEnvironment setting is crucial. When set to "node", Jest runs your tests within a Node.js context, allowing it to leverage Node’s native ESM loader. When set to "jsdom", it simulates a browser environment, and Jest uses a process that can handle ESM within that simulated DOM.

For Jest to correctly resolve imported file paths in ESM, you often need to include the file extension (e.g., ./math.js instead of ./math). This is a standard behavior in Node.js ESM resolution.

One critical detail is how Jest handles the exports field in package.json. When resolving modules, Jest respects the exports map if present, which allows package authors to define specific entry points for different environments (e.g., CommonJS vs. ESM). If your project or a dependency uses exports, Jest’s ESM resolver will correctly pick the appropriate subpath based on the current resolution context.

The next step is often integrating ESM tests with TypeScript, which requires a specific Jest transformer and tsconfig setup.

Want structured learning?

Take the full Jest course →