Repository navigation
Description
Version
v26.5.1 (behavior present from v22.x through main; see the source reference below)
Platform
Reproduced on darwin arm64; the code path is platform-independent.
Subsystem
module, esm
What steps will reproduce the bug?
mkdir -p repro/node_modules/dep/lib && cd repro
printf '{"name":"dep","exports":{".":"./lib/real.js","./sub":"./lib/sub.js"}}' > node_modules/dep/package.json
printf 'export const which = "real";' > node_modules/dep/lib/real.js
printf 'export const which = "sub";' > node_modules/dep/lib/sub.js
printf 'export const which = "decoy index.js";' > node_modules/dep/index.js
printf '{"type":"module"}' > package.json
cat > main.mjs <<'EOF'
const sub = await import("dep/sub").then(m => m.which, e => `${e.code}`);
const root = await import("dep").then(m => m.which, e => `${e.code}`);
console.log("dep/sub ->", sub);
console.log("dep ->", root);
EOF
node main.mjs
# dep/sub -> sub
# dep -> real
chmod 000 node_modules/dep/package.json # any read failure; EACCES stands in for EMFILE/EIO/ENOMEM
node main.mjs
# dep/sub -> ERR_MODULE_NOT_FOUND
# dep -> decoy index.jsWhat is the expected behavior?
A package.json that exists but cannot be read should surface the read error (or at minimum fail resolution with an error that names the errno), the way an unparsable package.json raises ERR_INVALID_PACKAGE_CONFIG.
What do you see instead?
The read failure is treated identically to the file being absent:
import("dep/sub")failsERR_MODULE_NOT_FOUNDfor a subpath the package exports — with aDid you mean to import "dep/lib/sub.js"?-style hint in real packages, pointing users away from the actual problem.import("dep")silently executes a different file than the package declares — legacyindex.jsresolution instead of theexportstarget. No error, no warning; wrong code runs.
The swallow is in src/node_modules.cc (GetPackageJSON):
if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
return nullptr;
}ReadFileSync's failure reason is discarded, so ENOENT (genuinely absent — the reasonable case to treat as "no package.json") is indistinguishable from EACCES, EMFILE, EIO, or ENOMEM on a file that exists.
Additional information
We hit this in CI as an unexplainable one-off: a correctly installed package with a correct exports map failed a subpath import with ERR_MODULE_NOT_FOUND on a heavily loaded runner, and everything on disk was byte-identical before and after. The root cause took a while to establish precisely because the errno is discarded — a transient read failure leaves no trace and masquerades as a missing module (taras/executable.md#439 has the investigation). Distinguishing ENOENT from other errnos at this one spot would turn silent misresolution into a diagnosable error.
Related but distinct: #54304 (over-long path makes the read fail, module type misdetected — same swallow surfacing through a different trigger), #49674 (misleading message when package.json exists but defines no matching export).
Activity
module: report unreadable package.json
bitpshr commentedon Aug 11, 2026
Opened #65223 for this.
GetPackageJSON()returned no config for any read failure, so an unreadablepackage.jsonwas indistinguishable from a missing one. ENOENT, ENOTDIR and EISDIR still mean "no package config here" (the last one covers apackage.jsonthat is itself a directory, whichtest-module-loading-errorrelies on); anything else now reports the underlying error.taras commentedon Aug 11, 2026
Awesome! thank you!
module: report unreadable package.json
module: report unreadable package.json
module: report unreadable package.json
module: report unreadable package.json
module: report unreadable package.json
module: report unreadable package.json
module: report unreadable package.json