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
- Use vi.mock('./mod', ...) at the top of the test file (hoisted) to replace the module's exports.
- Inside vi.mock's factory, return an object with the export replaced by vi.fn().
- If the export is a function used internally, refactor the source to import it from a dependency module you can mock.
- 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
- Prefer vi.mock() with a factory for replacing ESM named exports.
- Avoid import * as for modules you intend to spy on at runtime.
- Refactor source to consume injectable dependencies rather than reaching into another module's namespace.
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
- Cannot parse the module format of '${url}' because "module.f
- require() is not supported in virtual modules. Trying to cal
- ${utils.inspect(assertion._obj)} is not a spy or a call to a
- ${msg}
- ${formatCalls(spy, msg, args)}
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/e714ca69d975a0d6.json.
Report an issue: GitHub.