google/zx · error · Fail

Can't link node_modules: ${alias} already exists

Error message

Can't link node_modules: ${alias} already exists

What it means

Thrown by linkNodeModules() when the script's directory (cwd) already contains a real node_modules directory that differs from the requested symlink target. zx refuses to overwrite or destroy an existing real node_modules folder; it will only replace a missing entry or an existing symlink that points at the same target.

Source

Thrown at src/cli.ts:191

  }
}

function linkNodeModules(cwd: string, external: string): string {
  const nm = 'node_modules'
  const alias = path.resolve(cwd, nm)
  const target =
    path.basename(external) === nm
      ? path.resolve(external)
      : path.resolve(external, nm)
  const aliasStat = lstat(alias)
  const targetStat = lstat(target)

  if (!targetStat?.isDirectory())
    throw new Fail(
      `Can't link node_modules: ${target} doesn't exist or is not a directory`
    )
  if (aliasStat?.isDirectory() && alias !== target)
    throw new Fail(`Can't link node_modules: ${alias} already exists`)
  if (aliasStat) return ''

  fs.symlinkSync(target, alias, 'junction')
  return alias
}

function lstat(p: string) {
  try {
    return fs.lstatSync(p)
  } catch {}
}

async function readScript() {
  const [firstArg] = argv._
  let script = ''
  let scriptPath = ''
  let tempPath = ''
  let argSlice = 1

View on GitHub (pinned to 00a2c484e2)

Solutions

  1. Remove the existing node_modules in the script's directory: `rm -rf node_modules` (or `unlink node_modules` if it is a symlink).
  2. Run zx from a clean or temporary directory that has no node_modules.
  3. Drop the --prefer-local flag so zx does not attempt linking.
  4. Point --prefer-local at the same absolute path as the existing node_modules if you want the no-op path (alias === target returns '').

Example fix

// before: run.mjs sits in a dir that already has node_modules
$ zx --prefer-local=../ext run.mjs
// after: clear the conflicting dir first
$ rm -rf node_modules && zx --prefer-local=../ext run.mjs
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs'
import path from 'node:path'

function isSafeToLink(cwd: string, target: string): boolean {
  const alias = path.resolve(cwd, 'node_modules')
  if (alias === target) return true            // no-op link
  const stat = fs.lstatSync(alias)
  return !stat.isDirectory() || stat.isSymbolicLink()  // real dir blocks linking
}

if (!isSafeToLink(scriptDir, target)) {
  throw new Error(`remove existing node_modules in ${scriptDir} before --prefer-local`)
}

Try / catch

try {
  await runScript()
} catch (e) {
  if (e instanceof Fail && /already exists/.test(e.message)) {
    console.error('clean node_modules in the script dir first')
  }
  throw e
}

Prevention

When it happens

Trigger: Running a script with `--prefer-local=<external>` from a directory that already has its own `node_modules` real directory whose absolute path is not equal to the external target. Switching prefer-local targets without first removing the previously created link/dir also lands here.

Common situations: Running zx from inside a project root (which already ran npm install) with -l pointing elsewhere; monorepo where the script dir and the deps dir differ; stale node_modules left over from an earlier prefer-local run.

Related errors


AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13). Data as JSON: /api/errors/32e663edc50898e4. Report an issue: GitHub.