parcel-bundler/parcel · error · std::io::Error

NotFound

NotFound

Error message

Circular symlink

What it means

Thrown by CachedPath::canonicalize when the current thread is already in the middle of canonicalizing the same path. This is the cycle detector for symbolic links: each thread stamps a path with its thread id before recursing through parents/symlink targets, and re-encountering its own stamp means the symlink chain loops back on itself (a→b→a). The error is wrapped as std::io::ErrorKind::NotFound so the resolver treats the cycle as an unresolvable path.

Source

Thrown at crates/parcel-resolver/src/cache.rs:238

  }

  /// Returns whether the path is a node_modules directory.
  pub fn is_node_modules(&self) -> bool {
    self.0.flags.contains(PathFlags::IS_NODE_MODULES)
  }

  /// Returns whether the path is inside a node_modules directory.
  pub fn in_node_modules(&self) -> bool {
    self.0.flags.contains(PathFlags::IN_NODE_MODULES)
  }

  /// Returns the canonical path, resolving all symbolic links.
  pub fn canonicalize(&self, cache: &Cache) -> Result<CachedPath, ResolverError> {
    // Check if this thread is already canonicalizing. If so, we have found a circular symlink.
    // If a different thread is canonicalizing, OnceLock will queue this thread to wait for the result.
    let tid = THREAD_ID.with(|t| *t);
    if self.0.canonicalizing.load(Ordering::Acquire) == tid {
      return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Circular symlink").into());
    }

    self
      .0
      .canonical
      .get_or_init(|| {
        self.0.canonicalizing.store(tid, Ordering::Release);

        let res = self
          .parent()
          .map(|parent| {
            parent.canonicalize(cache).and_then(|parent_canonical| {
              let path = parent_canonical.join(
                self
                  .as_path()
                  .strip_prefix(parent.as_path())
                  .map_err(|_| ResolverError::UnknownError)?,
                cache,

View on GitHub (pinned to 59484858a1)

Solutions

  1. Find and remove the offending symlink cycle: run 'find <project_root> -type l' and inspect targets, or use 'symlinks -r <project_root>' / 'ls -lL' to expose loops.
  2. Reinstall node_modules (rm -rf node_modules && npm install) to clear corrupted package symlinks.
  3. If a symlink is intentional, ensure its target is outside the canonicalization chain (no ancestor/descendant relationship).
  4. Wrap the resolve call in try/catch and report the path so the user can locate the cycle.

Example fix

// before — resolver hits the cycle during canonicalization
const result = resolver.resolve({ filename, specifierType: 'esm', parent });
// error surfaces indirectly as a resolution failure

// after — detect and report the symlink cycle in your project
// from a shell, find and break the loop:
//   find . -type l -exec ls -l {} \; | awk '$NF ~ /\.$|loop/' 
// or reinstall to rebuild node_modules symlinks:
//   rm -rf node_modules && npm install
// then retry the resolve
try {
  const result = resolver.resolve({ filename, specifierType: 'esm', parent });
} catch (e) {
  throw new Error(`Resolution failed for ${filename}; possible symlink cycle under ${parent}. Run: find ${parent} -type l -exec ls -l {} \;`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

import { readdirSync, lstatSync, readlinkSync } from 'node:fs';
import { join } from 'node:path';
// Detect symlink cycles in a directory tree before resolving
function findSymlinkCycles(root, seen = new Set()) {
  for (const entry of readdirSync(root, { withFileTypes: true })) {
    const full = join(root, entry.name);
    if (entry.isSymbolicLink()) {
      let target = full, hops = 0;
      while (hops++ < 40) {
        target = readlinkSync(target);
        if (target === full || seen.has(target)) {
          return full; // found a cycle
        }
        seen.add(target);
      }
    } else if (entry.isDirectory()) {
      const found = findSymlinkCycles(full, seen);
      if (found) return found;
    }
  }
  return null;
}

Try / catch

try {
  return resolver.resolve(opts);
} catch (e) {
  if (e.message === 'Circular symlink' || e.code === 'NotFound') {
    throw new Error(`Symlink cycle detected under ${opts.parent}. Run: find ${opts.parent} -type l -exec ls -l {} \;`);
  }
  throw e;
}

Prevention

When it happens

Trigger: A filesystem contains a symlink cycle (symlink A points to B, B points to A, or A points to a descendant of itself) and the resolver attempts to canonicalize a path that traverses it. Also possible with self-referential symlinks (ln -s self self). Fires during any resolve call that walks through the cyclic symlink.

Common situations: Broken symlinks left over from a botched npm/pnpm install (especially pnpm's symlink-heavy node_modules layout); manually created symlinks that accidentally form a loop; a build tool or watcher that creates symlinks into its own output directory; monorepo workspace symlinks misconfigured to point upward.

Related errors


AI-assisted analysis of parcel-bundler/parcel@59484858a1 (2026-08-13). Data as JSON: /api/errors/add238d218dccc0e. Report an issue: GitHub.