pnpm/pnpm · warning · AggregateError

Failed to clean up replaced global installs

Error message

Failed to clean up replaced global installs

What it means

cleanupReplacedGlobalInstalls runs after activation already succeeded, removing installs that this activation replaced. Every group's removal is attempted even after failures; with exactly one failure that error is rethrown as-is, and with several the errors are wrapped in an AggregateError ('Failed to clean up replaced global installs'). The new install is live — only old directories or bins remain on disk.

Source

Thrown at pnpm11/global/commands/src/globalActivation.ts:95

    // Activation is already committed, so a leftover backup directory
    // must not fail the command — but it points at a filesystem problem
    // worth surfacing.
    globalWarn(`Failed to remove the global bin backup directory at ${prepared.backupDir}: ${getErrorMessage(err)}`)
  }
  return prepared.actualBinNames
}

export async function cleanupReplacedGlobalInstalls (
  opts: CleanupReplacedGlobalInstallsOptions
): Promise<void> {
  const errors: unknown[] = []
  for (const group of opts.groups) {
    // eslint-disable-next-line no-await-in-loop -- Cleanup mutations must settle before the next group starts.
    errors.push(...await cleanupReplacedGlobalInstall(opts, group))
  }
  if (errors.length === 1) throw errors[0]
  if (errors.length > 1) {
    throw new AggregateError(errors, 'Failed to clean up replaced global installs')
  }
}

// Activation already succeeded when this runs, so every removal is
// attempted even after one fails; the failures are aggregated instead of
// aborting the remaining cleanup.
async function cleanupReplacedGlobalInstall (
  opts: CleanupReplacedGlobalInstallsOptions,
  group: GlobalPackageInfo
): Promise<unknown[]> {
  const errors: unknown[] = []
  let binNames: string[]
  try {
    binNames = await getInstalledBinNames(group)
  } catch (err) {
    // The install directory is the only record of which bins the group
    // owns, so removing it now would strand them on PATH forever. Leave
    // the group intact for a later run to clean up.

View on GitHub (pinned to 5b11d3a15b)

Solutions

  1. Treat as non-fatal if the command you needed works: verify with `pnpm ls -g` and by running the binaries
  2. Close processes and terminals holding the old install paths, then re-run the global install so cleanup retries
  3. Delete the leftover replaced-install directories under the global package dir manually
Defensive patterns

Strategy: try-catch

Type guard

function isCleanupAggregate (err: unknown): err is AggregateError {
  return util.types.isNativeError(err) && Array.isArray((err as AggregateError).errors)
}

Try / catch

try {
  await activateAndCleanup(opts)
} catch (err) {
  if (isCleanupAggregate(err) && err.message === 'Failed to clean up replaced global installs') {
    // activation already succeeded: log err.errors and continue — verify bins run, clean leftovers later
    for (const e of err.errors) log.warn('cleanup failure:', e)
    return
  }
  throw err
}

Prevention

When it happens

Trigger: Post-activation removal of a replaced install directory or old bin fails: EBUSY/EPERM from locked files on Windows, read-only files, a terminal whose cwd is inside the removed directory, or a process holding an old shim open.

Common situations: Windows file locks on old shims during global update; editors or shells sitting inside the old install dir; leftovers from previous installs with unusual permissions.

Related errors


AI-assisted analysis of pnpm/pnpm@5b11d3a15b (2026-08-16). Data as JSON: /api/errors/cb535421cca59ac5. Report an issue: GitHub.