vitest-dev/vitest · error · TypeError

Cannot spy on export "${String(key)}". Module namespace is n

Error message

Cannot spy on export "${String(key)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/mocking/modules#mocking-a-module

What it means

Thrown by vi.spyOn() (packages/spy/src/index.ts) when reassigning the spied property fails with a TypeError on an object whose [Symbol.toStringTag] is 'Module'. ES module namespace objects are frozen and non-configurable, so you cannot redefine an export in place. Vitest catches the underlying 'Cannot redefine property'/'Cannot replace module namespace' error and rethrows with a pointer to the mocking guide, because the correct approach is vi.mock() at the module level.

Source

Thrown at packages/spy/src/index.ts:412

  })

  try {
    reassign(
      ssr
        ? () => mock
        : mock,
    )
  }
  catch (error) {
    if (
      error instanceof TypeError
      && Symbol.toStringTag
      && (object as any)[Symbol.toStringTag] === 'Module'
      && (error.message.includes('Cannot redefine property')
        || error.message.includes('Cannot replace module namespace')
        || error.message.includes('can\'t redefine non-configurable property'))
    ) {
      throw new TypeError(
        `Cannot spy on export "${String(key)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/mocking/modules#mocking-a-module`,
        { cause: error },
      )
    }

    throw error
  }

  return mock
}

function getDescriptor(obj: any, method: string | symbol | number): [any, PropertyDescriptor] | undefined {
  const objDescriptor = Object.getOwnPropertyDescriptor(obj, method)
  if (objDescriptor) {
    return [obj, objDescriptor]
  }
  let currentProto = Object.getPrototypeOf(obj)
  while (currentProto !== null) {

View on GitHub (pinned to d568f8ce37)

Solutions

  1. Use vi.mock('./mod', ...) at the top of the test file (hoisted) to replace the module's exports.
  2. Inside vi.mock's factory, return an object with the export replaced by vi.fn().
  3. If the export is a function used internally, refactor the source to import it from a dependency module you can mock.
  4. Avoid import * as for ESM and prefer named imports combined with vi.mock.

Example fix

// before
import * as utils from './utils'
vi.spyOn(utils, 'greet')

// after
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal()
  return { ...actual, greet: vi.fn() }
})
Defensive patterns

Strategy: validation

Validate before calling

function isModuleNamespace(obj: unknown): boolean {
  return typeof obj === 'object' && obj !== null
    && (obj as any)[Symbol.toStringTag] === 'Module'
}
if (isModuleNamespace(mod)) {
  // use vi.mock instead of vi.spyOn
}

Type guard

const isModuleNamespace = (obj: unknown): boolean =>
  typeof obj === 'object' && obj !== null && (obj as any)[Symbol.toStringTag] === 'Module'

Prevention

When it happens

Trigger: import * as mod from './mod'; vi.spyOn(mod, 'namedExport'); importing an ESM module as a namespace and trying to spy on one of its exports at runtime; spying on a CJS interop wrapper that presents as a Module namespace.

Common situations: Migrating from CommonJS to ESM where vi.spyOn used to work; trying to override a named export of a third-party ESM dependency; running tests with the ESM loader; spying before calling vi.mock() in a hoisted block.

Related errors


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/e714ca69d975a0d6.json. Report an issue: GitHub.