neoclide/coc.nvim · critical

coc.nvim requires Node.js VM modules support for ESM extensi

Error message

coc.nvim requires Node.js VM modules support for ESM extensions; start Node with --experimental-vm-modules

What it means

ensureVMModules guards ESM extension loading: Node's vm module must expose SourceTextModule and SyntheticModule, which only exist when Node runs with --experimental-vm-modules. Without that flag, loading ESM extensions inside the VM runtime is impossible, so it throws immediately.

Source

Thrown at src/extension/esm.ts:32

 * bridged with `vm.SyntheticModule`. Node must start with
 * `--experimental-vm-modules` for the VM module API to be available.
 */

export type ExtensionModuleFormat = 'commonjs' | 'module' | 'json' | 'native'

export type ResolvedExtensionModule =
  | { type: 'builtin'; id: string }
  | { type: 'coc-api'; id: 'coc.nvim' }
  | { type: 'file'; filename: string; format: ExtensionModuleFormat }

const Module: any = require('module')

/**
 * Fail only when ESM VM support is actually required.
 */
export function ensureVMModules(): void {
  if (typeof vm.SourceTextModule !== 'function' || typeof vm.SyntheticModule !== 'function') {
    throw new Error(
      'coc.nvim requires Node.js VM modules support for ESM extensions; ' +
      'start Node with --experimental-vm-modules'
    )
  }
}

const packageTypeCache = new Map<string, 'module' | 'commonjs' | undefined>()

function getPackageType(dirname: string): 'module' | 'commonjs' | undefined {
  let dir = dirname
  while (true) {
    let pkgFile = path.join(dir, 'package.json')
    if (fs.existsSync(pkgFile)) {
      let key = pkgFile
      if (packageTypeCache.has(key)) return packageTypeCache.get(key)
      let type: 'module' | 'commonjs' | undefined
      try {
        let obj = JSON.parse(fs.readFileSync(pkgFile, 'utf8'))

View on GitHub (pinned to 50e974d969)

Solutions

  1. Start Node with --experimental-vm-modules (e.g. set NODE_OPTIONS=--experimental-vm-modules).
  2. Upgrade to Node 22+ where VM modules no longer require the flag.
  3. Check coc's node binaryPath configuration points at the intended Node version.
  4. Alternatively, use the CommonJS build of the extension instead of its ESM entry.

Example fix

// before
$ vim  # node started without vm module support
// after
$ export NODE_OPTIONS="--experimental-vm-modules"
$ vim  # or use Node >= 22 which enables vm modules by default
Defensive patterns

Strategy: validation

Validate before calling

const vm = require('vm')
if (typeof vm.SourceTextModule !== 'function' || typeof vm.SyntheticModule !== 'function') {
  // set NODE_OPTIONS=--experimental-vm-modules or upgrade to Node >= 22 before loading ESM extensions
  process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ' --experimental-vm-modules'
}

Type guard

function hasVmModules(v: typeof import('vm')): boolean {
  return typeof (v as any).SourceTextModule === 'function' && typeof (v as any).SyntheticModule === 'function'
}

Try / catch

try {
  await loadEsmExtension(entry)
} catch (e) {
  if (e.message.includes('--experimental-vm-modules')) {
    logger.error('Restart Node with --experimental-vm-modules or use Node >= 22; falling back to CJS loader')
    return loadCjsExtension(entry)
  } else throw e
}

Prevention

When it happens

Trigger: Loading an ESM extension (loadSourceTextModule, createSyntheticModule, or loadESMEntry paths) on a Node.js build started without --experimental-vm-modules, or on a Node version where the flags/classes don't exist.

Common situations: Launching vim/neovim with a default node binary lacking the flag; using a Node version where the experimental flag was renamed/removed (Node >= 22 needs no flag, very old Node lacks support); custom node binaryPath in coc config missing the flag.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/5b7356496b8dac82. Report an issue: GitHub.