vercel/turborepo · error · Error
unexpected error reading "package.json" at ${workspaceRoot}
Error message
unexpected error reading "package.json" at ${workspaceRoot} What it means
Catch-all thrown by getPackageJson() in turbo-workspaces when reading package.json fails with an errno that is neither ENOENT nor EJSONPARSE - e.g. EACCES (no read permission), EISDIR (package.json is a directory), ENOTDIR (a component of workspaceRoot is a file), EMFILE/ELOOP (fd exhaustion or symlink cycles). Unlike its siblings it is a plain `new Error(...)`, not a ConvertError, so it carries no `type` field and will not match ConvertError-based handling; the path is embedded in the message.
Source
Thrown at packages/turbo-workspaces/src/utils.ts:77
try {
return readJsonSync(packageJsonPath, "utf8") as PackageJson;
} catch (err) {
if (err && typeof err === "object" && "code" in err) {
if (err.code === "ENOENT") {
throw new ConvertError(`no "package.json" found at ${workspaceRoot}`, {
type: "package_json-missing"
});
}
if (err.code === "EJSONPARSE") {
throw new ConvertError(
`failed to parse "package.json" at ${workspaceRoot}`,
{
type: "package_json-parse_error"
}
);
}
}
throw new Error(
`unexpected error reading "package.json" at ${workspaceRoot}`
);
}
}
function getWorkspacePackageManager({
workspaceRoot
}: {
workspaceRoot: string;
}): PackageManager | undefined {
const packageJson = getPackageJson({ workspaceRoot });
const { packageManager, devEngines } = packageJson;
if (packageManager) {
try {
const match = PACKAGE_MANAGER_REGEX.exec(packageManager);
if (match) {
const manager = match.groups?.manager;
return isPackageManager(manager) ? manager : undefined;View on GitHub (pinned to 9f94a7d215)
Solutions
- Check what workspaceRoot actually is: `ls -la <workspaceRoot>` and `ls -la <workspaceRoot>/package.json` - it must be a regular file inside real directories
- If it is EACCES/ownership: `chmod +r` the file and `chown` it (or run the command as the owning user/container user)
- If ENOTDIR: correct the root argument - it must stop at the directory containing package.json, not include a file name
- If EMFILE/ELOOP: raise ulimits (`ulimit -n 4096`) or remove the symlink cycle, close file-hungry processes, retry
Example fix
# before - a file segment inside the root path (ENOTDIR)
await getWorkspaceDetails({ root: "repo/README.md" });
# after - root is the directory that owns package.json
await getWorkspaceDetails({ root: "repo" }); Defensive patterns
Strategy: try-catch
Validate before calling
import { statSync, accessSync, constants } from "node:fs";
import path from "node:path";
const p = path.join(workspaceRoot, "package.json");
const st = statSync(p); // throws ENOENT/ENOTDIR/ELOOP early
if (!st.isFile()) throw new Error(`${p} is not a regular file`);
accessSync(p, constants.R_OK); // throws EACCES early Type guard
// This error is a plain Error (no ConvertError.type), so narrow by shape
function isUnexpectedPackageJsonRead(err: unknown): err is Error {
return err instanceof Error &&
!(err instanceof ConvertError) &&
err.message.includes('unexpected error reading "package.json"');
} Try / catch
try {
const details = await getWorkspaceDetails({ root });
} catch (err) {
if (err instanceof Error && err.message.includes('unexpected error reading "package.json"')) {
// inspect the path in the message: stat it, check permissions/ownership
console.error("Filesystem-level failure reading package.json - check perms and that the path is a regular file");
process.exit(1);
} else throw err;
} Prevention
- Validate that workspaceRoot is a directory and package.json is a regular readable file before calling in
- Run the tool as the user/container that owns the repo files
- Avoid symlink cycles in workspace roots; keep ulimit -n comfortable when watchers are active
- In Docker, ensure COPY did not materialize package.json as a directory
When it happens
Trigger: workspaceRoot containing a regular-file segment where a directory is expected (e.g. passing `repo/package.json` or `repo/somefile.ts` as root -> ENOTDIR); package.json created as a directory by a bad script or installer (EISDIR); POSIX file modes like 600 owned by another user, or a container/CI user without read access (EACCES); EMFILE from opening many files with watchers also running.
Common situations: Scripts that join the wrong segments when computing the project root; Docker stages that COPY a directory named package.json by accident; repositories restored from archives that lose permissions; heavy watcher setups exhausting file descriptors so even reads fail; symlinked workspace roots that loop.
Related errors
- Unable to write package.json
- package_json-missing
- Unable to write .gitignore
- Unable to read package.json
- error_removing_node_modules
AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16).
Data as JSON: /api/errors/40f83d757e29ab42.
Report an issue: GitHub.