Verify latest release
pnpm version
v11.4.0
Which area(s) of pnpm are affected? (leave empty if unsure)
No response
Link to the code that reproduces this issue or a replay of the bug
No response
Reproduction steps
A. Deterministic — proves the I/O pattern itself is racy
Save the following as repro.mjs and run node repro.mjs. It uses only node:fs and mirrors the exact pattern pnpm uses at updateWorkspaceState and loadWorkspaceState:
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const FILE = path.join(os.tmpdir(), "pnpm-race-demo-state.json");
const WORKERS = 8, ITERS = 500, PAYLOAD_KB = 200;
const payload = JSON.stringify({
projects: Array.from({ length: 50 }, (_, i) => ({ id: `pkg-${i}` })),
pad: "x".repeat(PAYLOAD_KB * 1024),
}, null, 2) + "\n";
// Mirrors pnpm's updateWorkspaceState — direct writeFile, NOT atomic.
const update = () => fs.writeFileSync(FILE, payload);
// Mirrors pnpm's loadWorkspaceState — handles ENOENT but NOT parse errors.
const load = () => {
let c;
try { c = fs.readFileSync(FILE, "utf-8"); }
catch (e) { if (e?.code === "ENOENT") return; throw e; }
return JSON.parse(c);
};
if (process.argv[2] === "--worker") {
for (let i = 0; i < ITERS; i++) {
update();
try { load(); }
catch (e) {
console.error(`worker ${process.pid} iter ${i}: ${e.message}`);
process.exit(1);
}
}
process.exit(0);
}
fs.writeFileSync(FILE, payload);
const procs = Array.from({ length: WORKERS }, () =>
spawn(process.execPath, [fileURLToPath(import.meta.url), "--worker"], { stdio: "inherit" }));
let crashes = 0;
await Promise.all(procs.map(p =>
new Promise(res => p.on("exit", code => { if (code !== 0) crashes++; res(); }))));
console.log(`Result: ${crashes}/${WORKERS} workers crashed.`);
process.exit(crashes > 0 ? 0 : 2);
ypical output on first run (Node 22, Linux, ~5 seconds):
worker 643 iter 0: Unexpected end of JSON input
worker 673 iter 1: Unexpected end of JSON input
worker 674 iter 1: Unexpected end of JSON input
worker 680 iter 1: Unexpected end of JSON input
worker 645 iter 12: Unterminated string in JSON at position 73728 (line 411 column 66418)
worker 681 iter 0: Unexpected end of JSON input
Result: 7/8 workers crashed.
(All errors are valid SyntaxError outcomes of JSON.parse on a partial read of a concurrent writeFile.)
B. Real-world — observed crash from a monorepo CI run
A monorepo where the build orchestrator fans out via pnpm run <target> per package (in our case via nx run-many --target=build --parallel=8) triggers the same crash unpredictably. The stack trace seen in CI:
pnpm: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at loadWorkspaceState (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:136939:19)
at checkDepsStatus (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:141974:71)
at runDepsStatusCheck (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:183766:91)
at Object.handler (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:183959:62)
at async main (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:193416:34)
at async runPnpm (/home/runner/setup-pnpm/.../pnpm/dist/pnpm.cjs:193683:5)
The race is intermittent because the window depends on disk speed, payload size, and process scheduling. But the underlying pattern (A) is provably unsafe.
Describe the Bug
When two or more pnpm processes run concurrently in the same workspace (common when a task runner like Nx, Turbo, or Lerna spawns pnpm run <target> per package in parallel), they can race on node_modules/.pnpm-workspace-state-v1.json and one of them crashes with SyntaxError: Unexpected end of JSON input inside loadWorkspaceState.
Even if a user wants verifyDepsBeforeRun enabled (the v11 default is "install"), pnpm should not crash because two of its own processes raced on a cache file it owns. A stale or corrupt cache should degrade gracefully, not abort the command. This also covers other corruption sources: a crashed/killed pnpm during a previous write, full disk during write, etc.
Expected Behavior
Concurrent pnpm invocations in the same workspace should not crash on the deps-status cache file. A partial or corrupt node_modules/.pnpm-workspace-state-v1.json should be treated as a cache miss (same behavior as ENOENT), and writes should be atomic so concurrent readers never observe a truncated file.
Which Node.js version are you using?
22.11.0
Which operating systems have you used?
If your OS is a Linux based, which one it is? (Include the version if relevant)
ubuntu-latest (Github CI)
Verify latest release
pnpm version
v11.4.0
Which area(s) of pnpm are affected? (leave empty if unsure)
No response
Link to the code that reproduces this issue or a replay of the bug
No response
Reproduction steps
A. Deterministic — proves the I/O pattern itself is racy
Save the following as
repro.mjsand runnode repro.mjs. It uses onlynode:fsand mirrors the exact pattern pnpm uses atupdateWorkspaceStateandloadWorkspaceState:ypical output on first run (Node 22, Linux, ~5 seconds):
(All errors are valid
SyntaxErroroutcomes ofJSON.parseon a partial read of a concurrentwriteFile.)B. Real-world — observed crash from a monorepo CI run
A monorepo where the build orchestrator fans out via
pnpm run <target>per package (in our case vianx run-many --target=build --parallel=8) triggers the same crash unpredictably. The stack trace seen in CI:The race is intermittent because the window depends on disk speed, payload size, and process scheduling. But the underlying pattern (A) is provably unsafe.
Describe the Bug
When two or more
pnpmprocesses run concurrently in the same workspace (common when a task runner like Nx, Turbo, or Lerna spawnspnpm run <target>per package in parallel), they can race onnode_modules/.pnpm-workspace-state-v1.jsonand one of them crashes withSyntaxError: Unexpected end of JSON inputinsideloadWorkspaceState.Even if a user wants
verifyDepsBeforeRunenabled (the v11 default is"install"), pnpm should not crash because two of its own processes raced on a cache file it owns. A stale or corrupt cache should degrade gracefully, not abort the command. This also covers other corruption sources: a crashed/killed pnpm during a previous write, full disk during write, etc.Expected Behavior
Concurrent
pnpminvocations in the same workspace should not crash on the deps-status cache file. A partial or corruptnode_modules/.pnpm-workspace-state-v1.jsonshould be treated as a cache miss (same behavior asENOENT), and writes should be atomic so concurrent readers never observe a truncated file.Which Node.js version are you using?
22.11.0
Which operating systems have you used?
If your OS is a Linux based, which one it is? (Include the version if relevant)
ubuntu-latest (Github CI)