neoclide/coc.nvim · error

${dest} exists, but not directory!

Error message

${dest} exists, but not directory!

What it means

After resolving dest, download() stats it if it already exists. If the path exists but is a regular file (or other non-directory), the download destination is unusable and it throws '<dest> exists, but not directory!'. dest is treated as the target directory for the downloaded (and possibly extracted) content.

Source

Thrown at src/model/download.ts:231

  for (let [name, value] of Object.entries({
    maxDownloadSize: options.maxDownloadSize,
    maxExtractSize: options.maxExtractSize,
    maxArchiveEntries: options.maxArchiveEntries
  })) {
    if (value !== undefined && (!Number.isFinite(value) || value <= 0)) throw new Error(`${name} must be a positive finite number`)
  }
  if (!dest || !path.isAbsolute(dest)) {
    throw new Error(`Invalid dest path: ${dest}`)
  }
  // Use one canonical lexical representation for filesystem operations and
  // archive-boundary checks. path.resolve also removes a trailing separator.
  dest = path.resolve(dest)
  if (!fs.existsSync(dest)) {
    fs.mkdirSync(dest, { recursive: true })
  } else {
    let stat = fs.statSync(dest)
    if (stat && !stat.isDirectory()) {
      throw new Error(`${dest} exists, but not directory!`)
    }
  }
  let mod = getRequestModule(url)
  let opts = resolveRequestOptions(url, options)
  if (!opts.agent && options.agent) opts.agent = options.agent
  let extname = path.extname(url.pathname)
  return new Promise<string>((resolve, reject) => {
    let timer: NodeJS.Timeout
    let settled = false
    let cancellation: { dispose(): void } | undefined
    const cleanup = (): void => {
      cancellation?.dispose()
      cancellation = undefined
      if (timer) clearTimeout(timer)
    }
    const succeed = (value: string): void => {
      if (settled) return
      settled = true

View on GitHub (pinned to 50e974d969)

Solutions

  1. Pass the parent directory as dest, not a file path: dest: '/opt/app' instead of '/opt/app/pkg.zip'.
  2. Remove or rename the conflicting file at dest, or pick a new directory.
  3. Check before calling: fs.statSync(dest).isDirectory() and clean up if false.

Example fix

// before
download({ url, dest: '/opt/app/tool.zip' }) // existing file
// after
download({ url, dest: '/opt/app' }) // dest is a directory
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs')
function assertExistingDirectory(p) {
  if (fs.existsSync(p) && !fs.statSync(p).isDirectory())
    throw new Error(`${p} exists but is not a directory`)
  return p
}

Type guard

function isDirectory(p: string): boolean { try { return fs.statSync(p).isDirectory() } catch { return false } }

Try / catch

try {
  await download({ url, dest })
} catch (e) {
  if (e.message.endsWith('exists, but not directory!')) {
    fs.rmSync(dest, { force: true }) // only if safe to delete
    return download({ url, dest })
  }
  throw e
}

Prevention

When it happens

Trigger: Calling download({ url, dest: '/opt/app/pkg.zip' }) where /opt/app/pkg.zip is an existing file, because dest was mistaken for the output file path rather than the output directory; a stale file occupies the intended directory name.

Common situations: Confusing dest (directory) with an output filepath; previous runs created a file with the same name as the intended directory; mount points or broken directories left by deployment tooling.

Related errors


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