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

  1. Check what workspaceRoot actually is: `ls -la <workspaceRoot>` and `ls -la <workspaceRoot>/package.json` - it must be a regular file inside real directories
  2. If it is EACCES/ownership: `chmod +r` the file and `chown` it (or run the command as the owning user/container user)
  3. If ENOTDIR: correct the root argument - it must stop at the directory containing package.json, not include a file name
  4. 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

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


AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16). Data as JSON: /api/errors/40f83d757e29ab42. Report an issue: GitHub.