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
- 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.
- Reinstall node_modules (rm -rf node_modules && npm install) to clear corrupted package symlinks.
- If a symlink is intentional, ensure its target is outside the canonicalization chain (no ancestor/descendant relationship).
- 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
- After install issues, rm -rf node_modules and reinstall to clear broken package symlinks (pnpm is especially symlink-heavy).
- Avoid creating symlinks that point into their own ancestor or descendant directories.
- Run 'find . -type l' periodically in projects with heavy symlinking to catch loops early.
- In CI, add a check that fails if the resolver reports NotFound on a known-good entry — it often masks a cycle.
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
- Unknown entry: ${entry}
- EINVAL
- ELOOP
- ${path.relative(process.cwd(), source)} is not a file.
- Could not find parcel config at ${path.relative(options.proj
AI-assisted analysis of parcel-bundler/parcel@59484858a1 (2026-08-13).
Data as JSON: /api/errors/add238d218dccc0e.
Report an issue: GitHub.